ring-client-api 13.0.1 → 13.2.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.js +4 -0
- package/lib/location.js +45 -34
- package/lib/rest-client.js +17 -10
- package/lib/ring-camera.js +29 -16
- package/lib/ring-chime.js +7 -1
- package/lib/ring-device.js +9 -0
- package/lib/ring-intercom.js +9 -3
- package/lib/ring-types.d.ts +5 -2
- package/lib/ring-types.js +4 -0
- package/lib/streaming/peer-connection.d.ts +1 -1
- package/lib/streaming/peer-connection.js +9 -8
- package/lib/streaming/simple-webrtc-session.js +3 -1
- package/lib/streaming/streaming-session.js +11 -9
- package/lib/streaming/webrtc-connection.js +16 -10
- package/lib/subscribed.js +1 -3
- package/lib/test/rest-client.spec.js +31 -32
- package/package.json +14 -13
package/lib/api.js
CHANGED
|
@@ -18,6 +18,9 @@ const push_receiver_1 = __importDefault(require("@eneris/push-receiver"));
|
|
|
18
18
|
const ring_intercom_1 = require("./ring-intercom");
|
|
19
19
|
const json_bigint_1 = __importDefault(require("json-bigint"));
|
|
20
20
|
class RingApi extends subscribed_1.Subscribed {
|
|
21
|
+
options;
|
|
22
|
+
restClient;
|
|
23
|
+
onRefreshTokenUpdated;
|
|
21
24
|
constructor(options) {
|
|
22
25
|
super();
|
|
23
26
|
this.options = options;
|
|
@@ -273,6 +276,7 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
273
276
|
});
|
|
274
277
|
return locations;
|
|
275
278
|
}
|
|
279
|
+
locationsPromise;
|
|
276
280
|
getLocations() {
|
|
277
281
|
if (!this.locationsPromise) {
|
|
278
282
|
this.locationsPromise = this.fetchAndBuildLocations();
|
package/lib/location.js
CHANGED
|
@@ -15,6 +15,51 @@ function flattenDeviceData(data) {
|
|
|
15
15
|
return Object.assign({}, data.general && data.general.v2, data.device && data.device.v1);
|
|
16
16
|
}
|
|
17
17
|
class Location extends subscribed_1.Subscribed {
|
|
18
|
+
locationDetails;
|
|
19
|
+
cameras;
|
|
20
|
+
chimes;
|
|
21
|
+
intercoms;
|
|
22
|
+
options;
|
|
23
|
+
restClient;
|
|
24
|
+
seq = 1;
|
|
25
|
+
onMessage = new rxjs_1.Subject();
|
|
26
|
+
onDataUpdate = new rxjs_1.Subject();
|
|
27
|
+
onDeviceDataUpdate = this.onDataUpdate.pipe((0, operators_1.filter)((message) => {
|
|
28
|
+
return message.datatype === 'DeviceInfoDocType' && Boolean(message.body);
|
|
29
|
+
}), (0, operators_1.concatMap)((message) => message.body), (0, operators_1.map)(flattenDeviceData));
|
|
30
|
+
onDeviceList = this.onMessage.pipe((0, operators_1.filter)((m) => m.msg === deviceListMessageType));
|
|
31
|
+
onDevices = this.onDeviceList.pipe((0, operators_1.scan)((devices, { body: deviceList, src }) => {
|
|
32
|
+
if (!deviceList) {
|
|
33
|
+
return devices;
|
|
34
|
+
}
|
|
35
|
+
if (!this.receivedAssetDeviceLists.includes(src)) {
|
|
36
|
+
this.receivedAssetDeviceLists.push(src);
|
|
37
|
+
}
|
|
38
|
+
return deviceList.reduce((updatedDevices, data) => {
|
|
39
|
+
const flatData = flattenDeviceData(data), existingDevice = updatedDevices.find((x) => x.zid === flatData.zid);
|
|
40
|
+
if (existingDevice) {
|
|
41
|
+
existingDevice.updateData(flatData);
|
|
42
|
+
return updatedDevices;
|
|
43
|
+
}
|
|
44
|
+
return [...updatedDevices, new ring_device_1.RingDevice(flatData, this, src)];
|
|
45
|
+
}, devices);
|
|
46
|
+
}, []), (0, operators_1.distinctUntilChanged)((a, b) => a.length === b.length), (0, operators_1.filter)(() => {
|
|
47
|
+
return Boolean(this.assets &&
|
|
48
|
+
this.assets.every((asset) => this.receivedAssetDeviceLists.includes(asset.uuid)));
|
|
49
|
+
}), (0, operators_1.shareReplay)(1));
|
|
50
|
+
onSessionInfo = this.onDataUpdate.pipe((0, operators_1.filter)((m) => m.msg === 'SessionInfo'), (0, operators_1.map)((m) => m.body));
|
|
51
|
+
onConnected = new rxjs_1.BehaviorSubject(false);
|
|
52
|
+
onLocationMode = new rxjs_1.ReplaySubject(1);
|
|
53
|
+
onLocationModeRequested = new rxjs_1.Subject();
|
|
54
|
+
reconnecting = false;
|
|
55
|
+
disconnected = false;
|
|
56
|
+
connectionPromise;
|
|
57
|
+
securityPanel;
|
|
58
|
+
assets;
|
|
59
|
+
receivedAssetDeviceLists = [];
|
|
60
|
+
offlineAssets = [];
|
|
61
|
+
hasHubs;
|
|
62
|
+
hasAlarmBaseStation;
|
|
18
63
|
constructor(locationDetails, cameras, chimes, intercoms, options, restClient) {
|
|
19
64
|
super();
|
|
20
65
|
this.locationDetails = locationDetails;
|
|
@@ -23,40 +68,6 @@ class Location extends subscribed_1.Subscribed {
|
|
|
23
68
|
this.intercoms = intercoms;
|
|
24
69
|
this.options = options;
|
|
25
70
|
this.restClient = restClient;
|
|
26
|
-
this.seq = 1;
|
|
27
|
-
this.onMessage = new rxjs_1.Subject();
|
|
28
|
-
this.onDataUpdate = new rxjs_1.Subject();
|
|
29
|
-
this.onDeviceDataUpdate = this.onDataUpdate.pipe((0, operators_1.filter)((message) => {
|
|
30
|
-
return message.datatype === 'DeviceInfoDocType' && Boolean(message.body);
|
|
31
|
-
}), (0, operators_1.concatMap)((message) => message.body), (0, operators_1.map)(flattenDeviceData));
|
|
32
|
-
this.onDeviceList = this.onMessage.pipe((0, operators_1.filter)((m) => m.msg === deviceListMessageType));
|
|
33
|
-
this.onDevices = this.onDeviceList.pipe((0, operators_1.scan)((devices, { body: deviceList, src }) => {
|
|
34
|
-
if (!deviceList) {
|
|
35
|
-
return devices;
|
|
36
|
-
}
|
|
37
|
-
if (!this.receivedAssetDeviceLists.includes(src)) {
|
|
38
|
-
this.receivedAssetDeviceLists.push(src);
|
|
39
|
-
}
|
|
40
|
-
return deviceList.reduce((updatedDevices, data) => {
|
|
41
|
-
const flatData = flattenDeviceData(data), existingDevice = updatedDevices.find((x) => x.zid === flatData.zid);
|
|
42
|
-
if (existingDevice) {
|
|
43
|
-
existingDevice.updateData(flatData);
|
|
44
|
-
return updatedDevices;
|
|
45
|
-
}
|
|
46
|
-
return [...updatedDevices, new ring_device_1.RingDevice(flatData, this, src)];
|
|
47
|
-
}, devices);
|
|
48
|
-
}, []), (0, operators_1.distinctUntilChanged)((a, b) => a.length === b.length), (0, operators_1.filter)(() => {
|
|
49
|
-
return Boolean(this.assets &&
|
|
50
|
-
this.assets.every((asset) => this.receivedAssetDeviceLists.includes(asset.uuid)));
|
|
51
|
-
}), (0, operators_1.shareReplay)(1));
|
|
52
|
-
this.onSessionInfo = this.onDataUpdate.pipe((0, operators_1.filter)((m) => m.msg === 'SessionInfo'), (0, operators_1.map)((m) => m.body));
|
|
53
|
-
this.onConnected = new rxjs_1.BehaviorSubject(false);
|
|
54
|
-
this.onLocationMode = new rxjs_1.ReplaySubject(1);
|
|
55
|
-
this.onLocationModeRequested = new rxjs_1.Subject();
|
|
56
|
-
this.reconnecting = false;
|
|
57
|
-
this.disconnected = false;
|
|
58
|
-
this.receivedAssetDeviceLists = [];
|
|
59
|
-
this.offlineAssets = [];
|
|
60
71
|
this.hasHubs = this.options.hasHubs;
|
|
61
72
|
this.hasAlarmBaseStation = this.options.hasAlarmBaseStation;
|
|
62
73
|
this.addSubscriptions(
|
package/lib/rest-client.js
CHANGED
|
@@ -138,6 +138,12 @@ function parseAuthConfig(rawRefreshToken) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
class RingRestClient {
|
|
141
|
+
authOptions;
|
|
142
|
+
refreshToken;
|
|
143
|
+
authConfig;
|
|
144
|
+
hardwareIdPromise;
|
|
145
|
+
_authPromise;
|
|
146
|
+
timeouts = [];
|
|
141
147
|
clearPreviousAuth() {
|
|
142
148
|
this._authPromise = undefined;
|
|
143
149
|
}
|
|
@@ -161,21 +167,22 @@ class RingRestClient {
|
|
|
161
167
|
}
|
|
162
168
|
return this._authPromise;
|
|
163
169
|
}
|
|
170
|
+
sessionPromise = undefined;
|
|
171
|
+
using2fa = false;
|
|
172
|
+
promptFor2fa;
|
|
173
|
+
onRefreshTokenUpdated = new rxjs_1.ReplaySubject(1);
|
|
174
|
+
onSession = new rxjs_1.ReplaySubject(1);
|
|
175
|
+
baseSessionMetadata;
|
|
164
176
|
constructor(authOptions) {
|
|
165
177
|
this.authOptions = authOptions;
|
|
166
|
-
this.refreshToken =
|
|
167
|
-
?
|
|
168
|
-
: undefined;
|
|
178
|
+
this.refreshToken =
|
|
179
|
+
'refreshToken' in authOptions ? authOptions.refreshToken : undefined;
|
|
169
180
|
this.authConfig = parseAuthConfig(this.refreshToken);
|
|
170
|
-
this.hardwareIdPromise =
|
|
171
|
-
|
|
172
|
-
this.sessionPromise = undefined;
|
|
173
|
-
this.using2fa = false;
|
|
174
|
-
this.onRefreshTokenUpdated = new rxjs_1.ReplaySubject(1);
|
|
175
|
-
this.onSession = new rxjs_1.ReplaySubject(1);
|
|
181
|
+
this.hardwareIdPromise =
|
|
182
|
+
this.authConfig?.hid || (0, util_1.getHardwareId)(authOptions.systemId);
|
|
176
183
|
this.baseSessionMetadata = {
|
|
177
184
|
api_version: apiVersion,
|
|
178
|
-
device_model:
|
|
185
|
+
device_model: authOptions.controlCenterDisplayName ?? 'ring-client-api',
|
|
179
186
|
};
|
|
180
187
|
}
|
|
181
188
|
getGrantData(twoFactorAuthCode) {
|
package/lib/ring-camera.js
CHANGED
|
@@ -68,28 +68,37 @@ function cleanSnapshotUuid(uuid) {
|
|
|
68
68
|
return uuid.replace(/:.*$/, '');
|
|
69
69
|
}
|
|
70
70
|
class RingCamera extends subscribed_1.Subscribed {
|
|
71
|
+
initialData;
|
|
72
|
+
isDoorbot;
|
|
73
|
+
restClient;
|
|
74
|
+
avoidSnapshotBatteryDrain;
|
|
75
|
+
id;
|
|
76
|
+
deviceType;
|
|
77
|
+
model;
|
|
78
|
+
onData;
|
|
79
|
+
hasLight;
|
|
80
|
+
hasSiren;
|
|
81
|
+
onRequestUpdate = new rxjs_1.Subject();
|
|
82
|
+
onNewNotification = new rxjs_1.Subject();
|
|
83
|
+
onActiveNotifications = new rxjs_1.BehaviorSubject([]);
|
|
84
|
+
onDoorbellPressed = this.onNewNotification.pipe((0, operators_1.filter)((notification) => notification.android_config.category === ring_types_1.PushNotificationAction.Ding), (0, operators_1.share)());
|
|
85
|
+
onMotionDetected = this.onActiveNotifications.pipe((0, operators_1.map)((notifications) => notifications.some((notification) => notification.android_config.category ===
|
|
86
|
+
ring_types_1.PushNotificationAction.Motion)), (0, operators_1.distinctUntilChanged)(), (0, operators_1.share)({
|
|
87
|
+
connector: () => new rxjs_1.ReplaySubject(1),
|
|
88
|
+
resetOnError: false,
|
|
89
|
+
resetOnComplete: false,
|
|
90
|
+
resetOnRefCountZero: false,
|
|
91
|
+
}));
|
|
92
|
+
onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.map)(() => null), // no value needed, event is what matters
|
|
93
|
+
(0, operators_1.share)());
|
|
94
|
+
onBatteryLevel;
|
|
95
|
+
onInHomeDoorbellStatus;
|
|
71
96
|
constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain) {
|
|
72
97
|
super();
|
|
73
98
|
this.initialData = initialData;
|
|
74
99
|
this.isDoorbot = isDoorbot;
|
|
75
100
|
this.restClient = restClient;
|
|
76
101
|
this.avoidSnapshotBatteryDrain = avoidSnapshotBatteryDrain;
|
|
77
|
-
this.onRequestUpdate = new rxjs_1.Subject();
|
|
78
|
-
this.onNewNotification = new rxjs_1.Subject();
|
|
79
|
-
this.onActiveNotifications = new rxjs_1.BehaviorSubject([]);
|
|
80
|
-
this.onDoorbellPressed = this.onNewNotification.pipe((0, operators_1.filter)((notification) => notification.android_config.category === ring_types_1.PushNotificationAction.Ding), (0, operators_1.share)());
|
|
81
|
-
this.onMotionDetected = this.onActiveNotifications.pipe((0, operators_1.map)((notifications) => notifications.some((notification) => notification.android_config.category ===
|
|
82
|
-
ring_types_1.PushNotificationAction.Motion)), (0, operators_1.distinctUntilChanged)(), (0, operators_1.share)({
|
|
83
|
-
connector: () => new rxjs_1.ReplaySubject(1),
|
|
84
|
-
resetOnError: false,
|
|
85
|
-
resetOnComplete: false,
|
|
86
|
-
resetOnRefCountZero: false,
|
|
87
|
-
}));
|
|
88
|
-
this.onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.map)(() => null), // no value needed, event is what matters
|
|
89
|
-
(0, operators_1.share)());
|
|
90
|
-
this.lastSnapshotTimestamp = 0;
|
|
91
|
-
this.lastSnapshotTimestampLocal = 0;
|
|
92
|
-
this.fetchingSnapshot = false;
|
|
93
102
|
this.id = this.initialData.id;
|
|
94
103
|
this.deviceType = this.initialData.kind;
|
|
95
104
|
this.model =
|
|
@@ -324,6 +333,9 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
324
333
|
? 600 * 1000 // battery cams only refresh timestamp every 10 minutes
|
|
325
334
|
: 10 * 1000; // snapshot updates will be forced. Limit to 10s lifetime
|
|
326
335
|
}
|
|
336
|
+
lastSnapshotTimestamp = 0;
|
|
337
|
+
lastSnapshotTimestampLocal = 0;
|
|
338
|
+
lastSnapshotPromise;
|
|
327
339
|
get currentTimestampAge() {
|
|
328
340
|
return Date.now() - this.lastSnapshotTimestampLocal;
|
|
329
341
|
}
|
|
@@ -352,6 +364,7 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
352
364
|
return false;
|
|
353
365
|
}
|
|
354
366
|
}
|
|
367
|
+
fetchingSnapshot = false;
|
|
355
368
|
async getSnapshot({ uuid } = {}) {
|
|
356
369
|
if (this.lastSnapshotPromise && this.shouldUseExistingSnapshotPromise()) {
|
|
357
370
|
return this.lastSnapshotPromise;
|
package/lib/ring-chime.js
CHANGED
|
@@ -11,10 +11,16 @@ const settingsWhichRequireReboot = [
|
|
|
11
11
|
'motion_audio_user_id',
|
|
12
12
|
];
|
|
13
13
|
class RingChime {
|
|
14
|
+
initialData;
|
|
15
|
+
restClient;
|
|
16
|
+
id;
|
|
17
|
+
deviceType;
|
|
18
|
+
model;
|
|
19
|
+
onData;
|
|
20
|
+
onRequestUpdate = new rxjs_1.Subject();
|
|
14
21
|
constructor(initialData, restClient) {
|
|
15
22
|
this.initialData = initialData;
|
|
16
23
|
this.restClient = restClient;
|
|
17
|
-
this.onRequestUpdate = new rxjs_1.Subject();
|
|
18
24
|
this.id = this.initialData.id;
|
|
19
25
|
this.deviceType = this.initialData.kind;
|
|
20
26
|
this.model = ring_types_1.ChimeModel[this.deviceType] || 'Chime';
|
package/lib/ring-device.js
CHANGED
|
@@ -7,6 +7,15 @@ const operators_1 = require("rxjs/operators");
|
|
|
7
7
|
const subscribed_1 = require("./subscribed");
|
|
8
8
|
const util_1 = require("./util");
|
|
9
9
|
class RingDevice extends subscribed_1.Subscribed {
|
|
10
|
+
initialData;
|
|
11
|
+
location;
|
|
12
|
+
assetId;
|
|
13
|
+
onData;
|
|
14
|
+
zid;
|
|
15
|
+
id;
|
|
16
|
+
deviceType;
|
|
17
|
+
categoryId;
|
|
18
|
+
onComponentDevices;
|
|
10
19
|
constructor(initialData, location, assetId) {
|
|
11
20
|
super();
|
|
12
21
|
this.initialData = initialData;
|
package/lib/ring-intercom.js
CHANGED
|
@@ -8,12 +8,18 @@ const operators_1 = require("rxjs/operators");
|
|
|
8
8
|
const ring_camera_1 = require("./ring-camera");
|
|
9
9
|
const util_1 = require("./util");
|
|
10
10
|
class RingIntercom {
|
|
11
|
+
initialData;
|
|
12
|
+
restClient;
|
|
13
|
+
id;
|
|
14
|
+
deviceType;
|
|
15
|
+
onData;
|
|
16
|
+
onRequestUpdate = new rxjs_1.Subject();
|
|
17
|
+
onBatteryLevel;
|
|
18
|
+
onDing = new rxjs_1.Subject();
|
|
19
|
+
onUnlocked = new rxjs_1.Subject();
|
|
11
20
|
constructor(initialData, restClient) {
|
|
12
21
|
this.initialData = initialData;
|
|
13
22
|
this.restClient = restClient;
|
|
14
|
-
this.onRequestUpdate = new rxjs_1.Subject();
|
|
15
|
-
this.onDing = new rxjs_1.Subject();
|
|
16
|
-
this.onUnlocked = new rxjs_1.Subject();
|
|
17
23
|
this.id = this.initialData.id;
|
|
18
24
|
this.deviceType = this.initialData.kind;
|
|
19
25
|
this.onData = new rxjs_1.BehaviorSubject(this.initialData);
|
package/lib/ring-types.d.ts
CHANGED
|
@@ -42,7 +42,8 @@ export declare enum RingDeviceType {
|
|
|
42
42
|
UnknownZWave = "unknown.zwave",
|
|
43
43
|
OnvifCamera = "onvif_camera",
|
|
44
44
|
ThirdPartyGarageDoorOpener = "third_party_gdo",
|
|
45
|
-
IntercomHandsetAudio = "intercom_handset_audio"
|
|
45
|
+
IntercomHandsetAudio = "intercom_handset_audio",
|
|
46
|
+
WaterValve = "valve.water"
|
|
46
47
|
}
|
|
47
48
|
export declare enum RingDeviceCategory {
|
|
48
49
|
Outlets = 1,
|
|
@@ -61,7 +62,8 @@ export declare enum RingDeviceCategory {
|
|
|
61
62
|
RangeExtenders = 32,
|
|
62
63
|
Keypads = 33,
|
|
63
64
|
Sirens = 34,
|
|
64
|
-
PanicButtons = 35
|
|
65
|
+
PanicButtons = 35,
|
|
66
|
+
WaterValves = 37
|
|
65
67
|
}
|
|
66
68
|
export declare enum RingCameraKind {
|
|
67
69
|
doorbot = "doorbot",
|
|
@@ -187,6 +189,7 @@ export interface RingDeviceData {
|
|
|
187
189
|
}[];
|
|
188
190
|
motionSensorEnabled?: boolean;
|
|
189
191
|
brightness?: number;
|
|
192
|
+
valveState?: 'open' | 'closed';
|
|
190
193
|
}
|
|
191
194
|
export declare const deviceTypesWithVolume: RingDeviceType[];
|
|
192
195
|
export interface BaseStation {
|
package/lib/ring-types.js
CHANGED
|
@@ -52,6 +52,7 @@ var RingDeviceType;
|
|
|
52
52
|
RingDeviceType["OnvifCamera"] = "onvif_camera";
|
|
53
53
|
RingDeviceType["ThirdPartyGarageDoorOpener"] = "third_party_gdo";
|
|
54
54
|
RingDeviceType["IntercomHandsetAudio"] = "intercom_handset_audio";
|
|
55
|
+
RingDeviceType["WaterValve"] = "valve.water";
|
|
55
56
|
})(RingDeviceType || (exports.RingDeviceType = RingDeviceType = {}));
|
|
56
57
|
// eslint-disable-next-line no-shadow
|
|
57
58
|
var RingDeviceCategory;
|
|
@@ -73,6 +74,7 @@ var RingDeviceCategory;
|
|
|
73
74
|
RingDeviceCategory[RingDeviceCategory["Keypads"] = 33] = "Keypads";
|
|
74
75
|
RingDeviceCategory[RingDeviceCategory["Sirens"] = 34] = "Sirens";
|
|
75
76
|
RingDeviceCategory[RingDeviceCategory["PanicButtons"] = 35] = "PanicButtons";
|
|
77
|
+
RingDeviceCategory[RingDeviceCategory["WaterValves"] = 37] = "WaterValves";
|
|
76
78
|
})(RingDeviceCategory || (exports.RingDeviceCategory = RingDeviceCategory = {}));
|
|
77
79
|
// eslint-disable-next-line no-shadow
|
|
78
80
|
var RingCameraKind;
|
|
@@ -110,6 +112,7 @@ var RingCameraKind;
|
|
|
110
112
|
RingCameraKind["stickup_cam_mini"] = "stickup_cam_mini";
|
|
111
113
|
RingCameraKind["onvif_camera"] = "onvif_camera";
|
|
112
114
|
})(RingCameraKind || (exports.RingCameraKind = RingCameraKind = {}));
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
113
116
|
exports.RingCameraModel = {
|
|
114
117
|
doorbot: 'Doorbell',
|
|
115
118
|
doorbell: 'Doorbell',
|
|
@@ -159,6 +162,7 @@ exports.deviceTypesWithVolume = [
|
|
|
159
162
|
RingDeviceType.BaseStation,
|
|
160
163
|
RingDeviceType.Keypad,
|
|
161
164
|
];
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
162
166
|
exports.ChimeModel = {
|
|
163
167
|
chime: 'Chime',
|
|
164
168
|
chime_pro: 'Chime Pro',
|
|
@@ -22,7 +22,7 @@ export declare class WeriftPeerConnection extends Subscribed implements BasicPee
|
|
|
22
22
|
onVideoRtp: Subject<RtpPacket>;
|
|
23
23
|
onVideoRtcp: Subject<RtcpPacket>;
|
|
24
24
|
onIceCandidate: Subject<RTCIceCandidate>;
|
|
25
|
-
onConnectionState: ReplaySubject<"
|
|
25
|
+
onConnectionState: ReplaySubject<"closed" | "connected" | "failed" | "disconnected" | "new" | "connecting">;
|
|
26
26
|
returnAudioTrack: MediaStreamTrack;
|
|
27
27
|
private onRequestKeyFrame;
|
|
28
28
|
constructor();
|
|
@@ -17,16 +17,17 @@ const ringIceServers = [
|
|
|
17
17
|
'stun:stun4.l.google.com:19302',
|
|
18
18
|
];
|
|
19
19
|
class WeriftPeerConnection extends subscribed_1.Subscribed {
|
|
20
|
+
pc;
|
|
21
|
+
onAudioRtp = new rxjs_1.Subject();
|
|
22
|
+
onAudioRtcp = new rxjs_1.Subject();
|
|
23
|
+
onVideoRtp = new rxjs_1.Subject();
|
|
24
|
+
onVideoRtcp = new rxjs_1.Subject();
|
|
25
|
+
onIceCandidate = new rxjs_1.Subject();
|
|
26
|
+
onConnectionState = new rxjs_1.ReplaySubject(1);
|
|
27
|
+
returnAudioTrack = new werift_1.MediaStreamTrack({ kind: 'audio' });
|
|
28
|
+
onRequestKeyFrame = new rxjs_1.Subject();
|
|
20
29
|
constructor() {
|
|
21
30
|
super();
|
|
22
|
-
this.onAudioRtp = new rxjs_1.Subject();
|
|
23
|
-
this.onAudioRtcp = new rxjs_1.Subject();
|
|
24
|
-
this.onVideoRtp = new rxjs_1.Subject();
|
|
25
|
-
this.onVideoRtcp = new rxjs_1.Subject();
|
|
26
|
-
this.onIceCandidate = new rxjs_1.Subject();
|
|
27
|
-
this.onConnectionState = new rxjs_1.ReplaySubject(1);
|
|
28
|
-
this.returnAudioTrack = new werift_1.MediaStreamTrack({ kind: 'audio' });
|
|
29
|
-
this.onRequestKeyFrame = new rxjs_1.Subject();
|
|
30
31
|
const pc = (this.pc = new werift_1.RTCPeerConnection({
|
|
31
32
|
codecs: {
|
|
32
33
|
audio: [
|
|
@@ -6,10 +6,12 @@ function liveViewUrl(path) {
|
|
|
6
6
|
return `https://api.ring.com/integrations/v1/liveview/${path}`;
|
|
7
7
|
}
|
|
8
8
|
class SimpleWebRtcSession {
|
|
9
|
+
camera;
|
|
10
|
+
restClient;
|
|
11
|
+
sessionId = (0, util_1.generateUuid)();
|
|
9
12
|
constructor(camera, restClient) {
|
|
10
13
|
this.camera = camera;
|
|
11
14
|
this.restClient = restClient;
|
|
12
|
-
this.sessionId = (0, util_1.generateUuid)();
|
|
13
15
|
}
|
|
14
16
|
async start(sdp) {
|
|
15
17
|
const response = await this.restClient.request({
|
|
@@ -17,19 +17,19 @@ function getCleanSdp(sdp, includeVideo) {
|
|
|
17
17
|
.join('\n');
|
|
18
18
|
}
|
|
19
19
|
class StreamingSession extends subscribed_1.Subscribed {
|
|
20
|
+
camera;
|
|
21
|
+
connection;
|
|
22
|
+
onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
23
|
+
onUsingOpus = new rxjs_1.ReplaySubject(1);
|
|
24
|
+
onVideoRtp = new rxjs_1.Subject();
|
|
25
|
+
onAudioRtp = new rxjs_1.Subject();
|
|
26
|
+
audioSplitter = new camera_utils_1.RtpSplitter();
|
|
27
|
+
videoSplitter = new camera_utils_1.RtpSplitter();
|
|
28
|
+
returnAudioSplitter = new camera_utils_1.RtpSplitter();
|
|
20
29
|
constructor(camera, connection) {
|
|
21
30
|
super();
|
|
22
31
|
this.camera = camera;
|
|
23
32
|
this.connection = connection;
|
|
24
|
-
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
25
|
-
this.onUsingOpus = new rxjs_1.ReplaySubject(1);
|
|
26
|
-
this.onVideoRtp = new rxjs_1.Subject();
|
|
27
|
-
this.onAudioRtp = new rxjs_1.Subject();
|
|
28
|
-
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
29
|
-
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
30
|
-
this.returnAudioSplitter = new camera_utils_1.RtpSplitter();
|
|
31
|
-
this.cameraSpeakerActivated = false;
|
|
32
|
-
this.hasEnded = false;
|
|
33
33
|
this.bindToConnection(connection);
|
|
34
34
|
}
|
|
35
35
|
bindToConnection(connection) {
|
|
@@ -44,6 +44,7 @@ class StreamingSession extends subscribed_1.Subscribed {
|
|
|
44
44
|
activate() {
|
|
45
45
|
this.requestKeyFrame();
|
|
46
46
|
}
|
|
47
|
+
cameraSpeakerActivated = false;
|
|
47
48
|
activateCameraSpeaker() {
|
|
48
49
|
if (this.cameraSpeakerActivated || this.hasEnded) {
|
|
49
50
|
return;
|
|
@@ -155,6 +156,7 @@ class StreamingSession extends subscribed_1.Subscribed {
|
|
|
155
156
|
});
|
|
156
157
|
this.onCallEnded.pipe((0, operators_1.take)(1)).subscribe(() => ff.stop());
|
|
157
158
|
}
|
|
159
|
+
hasEnded = false;
|
|
158
160
|
callEnded() {
|
|
159
161
|
if (this.hasEnded) {
|
|
160
162
|
return;
|
|
@@ -8,19 +8,23 @@ const util_1 = require("../util");
|
|
|
8
8
|
const peer_connection_1 = require("./peer-connection");
|
|
9
9
|
const subscribed_1 = require("../subscribed");
|
|
10
10
|
class WebrtcConnection extends subscribed_1.Subscribed {
|
|
11
|
+
camera;
|
|
12
|
+
onSessionId = new rxjs_1.ReplaySubject(1);
|
|
13
|
+
onOfferSent = new rxjs_1.ReplaySubject(1);
|
|
14
|
+
dialogId = (0, util_1.generateUuid)();
|
|
15
|
+
onCameraConnected = new rxjs_1.ReplaySubject(1);
|
|
16
|
+
onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
17
|
+
onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
18
|
+
onError = new rxjs_1.ReplaySubject(1);
|
|
19
|
+
onMessage = new rxjs_1.ReplaySubject();
|
|
20
|
+
onWsOpen;
|
|
21
|
+
onAudioRtp;
|
|
22
|
+
onVideoRtp;
|
|
23
|
+
pc;
|
|
24
|
+
ws;
|
|
11
25
|
constructor(ticket, camera, options) {
|
|
12
26
|
super();
|
|
13
27
|
this.camera = camera;
|
|
14
|
-
this.onSessionId = new rxjs_1.ReplaySubject(1);
|
|
15
|
-
this.onOfferSent = new rxjs_1.ReplaySubject(1);
|
|
16
|
-
this.dialogId = (0, util_1.generateUuid)();
|
|
17
|
-
this.onCameraConnected = new rxjs_1.ReplaySubject(1);
|
|
18
|
-
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
19
|
-
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
20
|
-
this.onError = new rxjs_1.ReplaySubject(1);
|
|
21
|
-
this.onMessage = new rxjs_1.ReplaySubject();
|
|
22
|
-
this.sessionId = null;
|
|
23
|
-
this.hasEnded = false;
|
|
24
28
|
this.ws = new ws_1.WebSocket(`wss://api.prod.signalling.ring.devices.a2z.com:443/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-${(0, util_1.generateUuid)()}&token=${ticket}`, {
|
|
25
29
|
headers: {
|
|
26
30
|
// This must exist or the socket will close immediately but content does not seem to matter
|
|
@@ -110,6 +114,7 @@ class WebrtcConnection extends subscribed_1.Subscribed {
|
|
|
110
114
|
});
|
|
111
115
|
this.onOfferSent.next();
|
|
112
116
|
}
|
|
117
|
+
sessionId = null;
|
|
113
118
|
async handleMessage(message) {
|
|
114
119
|
if (message.body.doorbot_id !== this.camera.id) {
|
|
115
120
|
// ignore messages for other cameras
|
|
@@ -220,6 +225,7 @@ class WebrtcConnection extends subscribed_1.Subscribed {
|
|
|
220
225
|
});
|
|
221
226
|
}));
|
|
222
227
|
}
|
|
228
|
+
hasEnded = false;
|
|
223
229
|
callEnded() {
|
|
224
230
|
if (this.hasEnded) {
|
|
225
231
|
return;
|
package/lib/subscribed.js
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Subscribed = void 0;
|
|
4
4
|
class Subscribed {
|
|
5
|
-
|
|
6
|
-
this.subscriptions = [];
|
|
7
|
-
}
|
|
5
|
+
subscriptions = [];
|
|
8
6
|
addSubscriptions(...subscriptions) {
|
|
9
7
|
this.subscriptions.push(...subscriptions);
|
|
10
8
|
}
|
|
@@ -6,105 +6,104 @@ const rest_client_1 = require("../rest-client");
|
|
|
6
6
|
const util_1 = require("../util");
|
|
7
7
|
const rxjs_1 = require("rxjs");
|
|
8
8
|
let sessionCreatedCount = 0, client;
|
|
9
|
-
const email = 'some@one.com', password = 'abc123!', phone = '+1xxxxxxxx89', twoFactorAuthCode = '123456', hardwareIdPromise = (0, util_1.getHardwareId)(), accessToken = 'ey__accees_token', secondAccessToken = 'ey__second_accees_token', refreshToken = 'ey__refresh_token', secondRefreshToken = 'ey__second_refresh_token', thirdRefreshToken = 'ey__third_refresh_token', server = (0, node_1.setupServer)(msw_1.
|
|
9
|
+
const email = 'some@one.com', password = 'abc123!', phone = '+1xxxxxxxx89', twoFactorAuthCode = '123456', hardwareIdPromise = (0, util_1.getHardwareId)(), accessToken = 'ey__accees_token', secondAccessToken = 'ey__second_accees_token', refreshToken = 'ey__refresh_token', secondRefreshToken = 'ey__second_refresh_token', thirdRefreshToken = 'ey__third_refresh_token', server = (0, node_1.setupServer)(msw_1.http.post('https://oauth.ring.com/oauth/token', async ({ request: req }) => {
|
|
10
10
|
const body = await req.json();
|
|
11
11
|
if (req.headers.get('2fa-support') !== 'true' ||
|
|
12
12
|
req.headers.get('User-Agent') !== 'android:com.ringapp' ||
|
|
13
13
|
req.headers.get('hardware_id') !== (await hardwareIdPromise)) {
|
|
14
|
-
return
|
|
14
|
+
return msw_1.HttpResponse.json({
|
|
15
15
|
code: 1,
|
|
16
|
-
error: 'Invalid auth headers: ' +
|
|
17
|
-
|
|
18
|
-
}));
|
|
16
|
+
error: 'Invalid auth headers: ' + JSON.stringify(req.headers, null, 2),
|
|
17
|
+
}, { status: 400 });
|
|
19
18
|
}
|
|
20
19
|
if (body.grant_type === 'refresh_token') {
|
|
21
20
|
if (body.refresh_token === refreshToken) {
|
|
22
21
|
// Valid refresh token
|
|
23
|
-
return
|
|
22
|
+
return msw_1.HttpResponse.json({
|
|
24
23
|
access_token: accessToken,
|
|
25
24
|
expires_in: 3600,
|
|
26
25
|
refresh_token: secondRefreshToken,
|
|
27
26
|
scope: 'client',
|
|
28
27
|
token_type: 'Bearer',
|
|
29
|
-
})
|
|
28
|
+
}, { status: 200 });
|
|
30
29
|
}
|
|
31
30
|
if (body.refresh_token === secondRefreshToken) {
|
|
32
31
|
// Valid refresh token
|
|
33
|
-
return
|
|
32
|
+
return msw_1.HttpResponse.json({
|
|
34
33
|
access_token: secondAccessToken,
|
|
35
34
|
expires_in: 3600,
|
|
36
35
|
refresh_token: thirdRefreshToken,
|
|
37
36
|
scope: 'client',
|
|
38
37
|
token_type: 'Bearer',
|
|
39
|
-
})
|
|
38
|
+
}, { status: 200 });
|
|
40
39
|
}
|
|
41
40
|
// Invalid refresh token
|
|
42
|
-
return
|
|
41
|
+
return msw_1.HttpResponse.json({
|
|
43
42
|
error: 'invalid_grant',
|
|
44
43
|
error_description: 'token is invalid or does not exists',
|
|
45
|
-
})
|
|
44
|
+
}, { status: 401 });
|
|
46
45
|
}
|
|
47
46
|
if (body.grant_type !== 'password' ||
|
|
48
47
|
body.client_id !== 'ring_official_android' ||
|
|
49
48
|
body.scope !== 'client') {
|
|
50
|
-
return
|
|
49
|
+
return msw_1.HttpResponse.json({
|
|
51
50
|
code: 1,
|
|
52
51
|
error: 'Invalid auth request: ' + JSON.stringify(body),
|
|
53
|
-
})
|
|
52
|
+
}, { status: 400 });
|
|
54
53
|
}
|
|
55
54
|
if (body.username !== email || body.password !== password) {
|
|
56
55
|
// Wrong username or password
|
|
57
|
-
return
|
|
56
|
+
return msw_1.HttpResponse.json({
|
|
58
57
|
error: 'access_denied',
|
|
59
58
|
error_description: 'invalid user credentials',
|
|
60
|
-
})
|
|
59
|
+
}, { status: 401 });
|
|
61
60
|
}
|
|
62
61
|
if (req.headers.get('2fa-code') &&
|
|
63
62
|
req.headers.get('2fa-code') !== twoFactorAuthCode) {
|
|
64
63
|
// Wrong 2fa code
|
|
65
|
-
return
|
|
64
|
+
return msw_1.HttpResponse.json({
|
|
66
65
|
err_msg: 'bad request response from dependency service',
|
|
67
66
|
error: 'Verification Code is invalid or expired',
|
|
68
|
-
})
|
|
67
|
+
}, { status: 400 });
|
|
69
68
|
}
|
|
70
69
|
if (req.headers.get('2fa-code') === twoFactorAuthCode) {
|
|
71
70
|
// Successfull login with correct 2fa code
|
|
72
|
-
return
|
|
71
|
+
return msw_1.HttpResponse.json({
|
|
73
72
|
access_token: accessToken,
|
|
74
73
|
expires_in: 3600,
|
|
75
74
|
refresh_token: refreshToken,
|
|
76
75
|
scope: 'client',
|
|
77
76
|
token_type: 'Bearer',
|
|
78
77
|
responseTimestamp: Date.now(),
|
|
79
|
-
})
|
|
78
|
+
});
|
|
80
79
|
}
|
|
81
80
|
// 2fa code not provided, so return the 2fa prompt
|
|
82
|
-
return
|
|
81
|
+
return msw_1.HttpResponse.json({
|
|
83
82
|
next_time_in_secs: 60,
|
|
84
83
|
phone,
|
|
85
84
|
tsv_state: 'sms',
|
|
86
|
-
})
|
|
87
|
-
}), msw_1.
|
|
85
|
+
}, { status: 412 });
|
|
86
|
+
}), msw_1.http.post('https://api.ring.com/clients_api/session', async ({ request: req }) => {
|
|
88
87
|
const authHeader = req.headers.get('Authorization');
|
|
89
88
|
if (authHeader !== `Bearer ${accessToken}` &&
|
|
90
89
|
authHeader !== `Bearer ${secondAccessToken}`) {
|
|
91
90
|
// Invalid access token used
|
|
92
|
-
return
|
|
91
|
+
return msw_1.HttpResponse.json({}, { status: 401 });
|
|
93
92
|
}
|
|
94
93
|
const body = await req.json();
|
|
95
94
|
if (body.device.hardware_id !== (await (0, util_1.getHardwareId)()) ||
|
|
96
95
|
body.device.metadata.api_version !== 11 ||
|
|
97
96
|
body.device.metadata.device_model !== 'ring-client-api' ||
|
|
98
97
|
body.device.os !== 'android') {
|
|
99
|
-
return
|
|
98
|
+
return msw_1.HttpResponse.text('Bad session request: ' + JSON.stringify(body, null, 2), { status: 400 });
|
|
100
99
|
}
|
|
101
100
|
// Fake a response from the session endpoint, incrementing the sessionCreatedCount
|
|
102
101
|
sessionCreatedCount++;
|
|
103
|
-
return
|
|
102
|
+
return msw_1.HttpResponse.json({
|
|
104
103
|
profile: {
|
|
105
104
|
id: 1234,
|
|
106
105
|
},
|
|
107
|
-
})
|
|
106
|
+
});
|
|
108
107
|
}));
|
|
109
108
|
async function wrapRefreshToken(rt) {
|
|
110
109
|
return (0, util_1.toBase64)(JSON.stringify({
|
|
@@ -200,25 +199,25 @@ describe('fetch', () => {
|
|
|
200
199
|
let invalidateFirstAccessToken = false;
|
|
201
200
|
beforeEach(() => {
|
|
202
201
|
invalidateFirstAccessToken = false;
|
|
203
|
-
server.use(msw_1.
|
|
202
|
+
server.use(msw_1.http.get('https://api.ring.com/clients_api/some_endpoint', ({ request: req }) => {
|
|
204
203
|
const authHeader = req.headers.get('Authorization');
|
|
205
204
|
if (invalidateFirstAccessToken &&
|
|
206
205
|
authHeader === `Bearer ${accessToken}`) {
|
|
207
206
|
// Original access token used, but no longer valid
|
|
208
|
-
return
|
|
207
|
+
return msw_1.HttpResponse.json({}, { status: 401 });
|
|
209
208
|
}
|
|
210
209
|
if (authHeader !== `Bearer ${accessToken}` &&
|
|
211
210
|
authHeader !== `Bearer ${secondAccessToken}`) {
|
|
212
211
|
// Invalid access token used
|
|
213
|
-
return
|
|
212
|
+
return msw_1.HttpResponse.json({}, { status: 401 });
|
|
214
213
|
}
|
|
215
214
|
if (sessionCreatedCount === 0) {
|
|
216
215
|
// Session not created yet
|
|
217
|
-
return
|
|
216
|
+
return msw_1.HttpResponse.json({
|
|
218
217
|
error: 'Session not found for ' + req.headers.get('hardware_id'),
|
|
219
|
-
})
|
|
218
|
+
}, { status: 404 });
|
|
220
219
|
}
|
|
221
|
-
return
|
|
220
|
+
return msw_1.HttpResponse.json([]);
|
|
222
221
|
}));
|
|
223
222
|
});
|
|
224
223
|
it('should include the auth token as a header', async () => {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ring-client-api",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.2.0-beta.0",
|
|
4
4
|
"description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./lib/index.js",
|
|
8
8
|
"./ffmpeg": "./lib/ffmpeg.js",
|
|
9
9
|
"./util": "./lib/util.js",
|
|
10
|
-
"./rest-client": "./lib/rest-client.js"
|
|
10
|
+
"./rest-client": "./lib/rest-client.js",
|
|
11
|
+
"./streaming/streaming-session": "./lib/streaming/streaming-session.js"
|
|
11
12
|
},
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "rm -rf lib && tsc --declaration && chmod +x ./lib/ring-*-cli.js",
|
|
@@ -17,35 +18,35 @@
|
|
|
17
18
|
"test:watch": "jest --watch"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@eneris/push-receiver": "4.
|
|
21
|
+
"@eneris/push-receiver": "4.2.0",
|
|
21
22
|
"@homebridge/camera-utils": "^2.2.6",
|
|
22
|
-
"@types/socket.io-client": "1.4.36",
|
|
23
23
|
"colors": "1.4.0",
|
|
24
|
-
"debug": "^4.3.
|
|
24
|
+
"debug": "^4.3.7",
|
|
25
25
|
"json-bigint": "^1.0.0",
|
|
26
26
|
"rxjs": "^7.8.1",
|
|
27
27
|
"socket.io-client": "^2.5.0",
|
|
28
|
-
"systeminformation": "^5.
|
|
28
|
+
"systeminformation": "^5.23.5",
|
|
29
29
|
"uuid": "^10.0.0",
|
|
30
30
|
"werift": "0.19.4",
|
|
31
31
|
"ws": "^8.18.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/debug": "4.1.12",
|
|
35
|
-
"@types/jest": "29.5.
|
|
35
|
+
"@types/jest": "29.5.13",
|
|
36
36
|
"@types/json-bigint": "^1.0.4",
|
|
37
|
-
"@types/node": "
|
|
37
|
+
"@types/node": "22.7.5",
|
|
38
|
+
"@types/socket.io-client": "1.4.36",
|
|
38
39
|
"@types/uuid": "10.0.0",
|
|
39
|
-
"@types/ws": "^8.5.
|
|
40
|
+
"@types/ws": "^8.5.12",
|
|
40
41
|
"eslint-config-shared": "*",
|
|
41
42
|
"jest": "29.7.0",
|
|
42
|
-
"msw": "^
|
|
43
|
-
"ts-jest": "29.2.
|
|
43
|
+
"msw": "^2.4.10",
|
|
44
|
+
"ts-jest": "29.2.5",
|
|
44
45
|
"tsconfig": "*",
|
|
45
|
-
"typescript": "5.
|
|
46
|
+
"typescript": "5.6.3"
|
|
46
47
|
},
|
|
47
48
|
"engines": {
|
|
48
|
-
"node": "^18 || ^20"
|
|
49
|
+
"node": "^18 || ^20 || ^22"
|
|
49
50
|
},
|
|
50
51
|
"repository": {
|
|
51
52
|
"type": "git",
|