ring-client-api 13.0.0-beta.1 → 13.0.0-beta.3

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 CHANGED
@@ -200,13 +200,21 @@ class RingApi extends subscribed_1.Subscribed {
200
200
  messageData[p] = message.data[p];
201
201
  }
202
202
  }
203
- const notification = messageData, deviceId = notification.data?.device?.id;
203
+ const notification = messageData;
204
+ if (!('android_config' in notification)) {
205
+ // This is not a v2 ding style notification, so we can't process it
206
+ (0, util_1.logInfo)('Received push notification in unknown format');
207
+ (0, util_1.logInfo)(JSON.stringify(message));
208
+ return;
209
+ }
210
+ const deviceId = notification.data?.device?.id;
204
211
  if (deviceId) {
205
212
  sendToDevice(deviceId, notification);
206
213
  }
207
214
  const eventCategory = notification.android_config.category;
208
215
  if (eventCategory !== ring_types_1.PushNotificationAction.Ding &&
209
- eventCategory !== ring_types_1.PushNotificationAction.Motion) {
216
+ eventCategory !== ring_types_1.PushNotificationAction.Motion &&
217
+ eventCategory !== ring_types_1.PushNotificationAction.IntercomDing) {
210
218
  (0, util_1.logInfo)('Received push notification with unknown category: ' +
211
219
  eventCategory);
212
220
  (0, util_1.logInfo)(JSON.stringify(message));
@@ -1,7 +1,11 @@
1
- import { Options as RequestOptions } from 'got';
2
1
  import { AuthTokenResponse, SessionResponse } from './ring-types';
3
2
  import { ReplaySubject } from 'rxjs';
4
3
  import type { Credentials } from '@eneris/push-receiver/dist/types';
4
+ interface RequestOptions extends RequestInit {
5
+ responseType?: 'json' | 'buffer';
6
+ timeout?: number;
7
+ json?: object;
8
+ }
5
9
  export declare function clientApi(path: string): string;
6
10
  export declare function deviceApi(path: string): string;
7
11
  export declare function commandsApi(path: string): string;
@@ -59,3 +63,4 @@ export declare class RingRestClient {
59
63
  get _internalOnly_pushNotificationCredentials(): Credentials | undefined;
60
64
  set _internalOnly_pushNotificationCredentials(credentials: Credentials | undefined);
61
65
  }
66
+ export {};
@@ -8,14 +8,12 @@ exports.clientApi = clientApi;
8
8
  exports.deviceApi = deviceApi;
9
9
  exports.commandsApi = commandsApi;
10
10
  exports.appApi = appApi;
11
- const got_1 = __importDefault(require("got"));
12
11
  const util_1 = require("./util");
13
12
  const rxjs_1 = require("rxjs");
14
13
  const assert_1 = __importDefault(require("assert"));
15
14
  const defaultRequestOptions = {
16
15
  responseType: 'json',
17
16
  method: 'GET',
18
- retry: 0,
19
17
  timeout: 20000,
20
18
  }, ringErrorCodes = {
21
19
  7050: 'NO_ASSET',
@@ -36,18 +34,76 @@ function commandsApi(path) {
36
34
  function appApi(path) {
37
35
  return appApiBaseUrl + path;
38
36
  }
37
+ async function responseToError(response) {
38
+ const error = new Error();
39
+ error.response = {
40
+ headers: response.headers,
41
+ status: response.status,
42
+ body: null,
43
+ };
44
+ try {
45
+ const bodyText = await response.text();
46
+ try {
47
+ error.response.body = JSON.parse(bodyText);
48
+ }
49
+ catch (_) {
50
+ error.response.body = bodyText;
51
+ }
52
+ }
53
+ catch (_) {
54
+ // ignore
55
+ }
56
+ return error;
57
+ }
39
58
  async function requestWithRetry(requestOptions) {
40
59
  try {
60
+ if (requestOptions.json || requestOptions.responseType === 'json') {
61
+ requestOptions.headers = {
62
+ ...requestOptions.headers,
63
+ 'Content-Type': 'application/json',
64
+ Accept: 'application/json',
65
+ };
66
+ if (requestOptions.json) {
67
+ requestOptions.body = JSON.stringify(requestOptions.json);
68
+ }
69
+ delete requestOptions.json;
70
+ }
41
71
  const options = {
42
72
  ...defaultRequestOptions,
43
73
  ...requestOptions,
44
- }, { headers, body } = (await (0, got_1.default)(options)), data = body;
74
+ };
75
+ // If a timeout is provided, create an AbortSignal for it
76
+ if (options.timeout && !options.signal) {
77
+ options.signal = AbortSignal.timeout(options.timeout);
78
+ }
79
+ // make the fetch request
80
+ const response = await fetch(options.url, options), headers = response.headers;
81
+ if (!response.ok) {
82
+ const error = await responseToError(response);
83
+ throw error;
84
+ }
85
+ let data;
86
+ if (options.responseType === 'buffer') {
87
+ const arrayBuffer = await response.arrayBuffer();
88
+ data = Buffer.from(arrayBuffer);
89
+ }
90
+ else {
91
+ const text = await response.text();
92
+ try {
93
+ data = JSON.parse(text);
94
+ }
95
+ catch (_) {
96
+ data = text;
97
+ }
98
+ }
45
99
  if (data !== null && typeof data === 'object') {
46
- if (headers.date) {
47
- data.responseTimestamp = new Date(headers.date).getTime();
100
+ const date = headers.get('date');
101
+ if (date) {
102
+ data.responseTimestamp = new Date(date).getTime();
48
103
  }
49
- if (headers['x-time-millis']) {
50
- data.timeMillis = Number(headers['x-time-millis']);
104
+ const xTime = headers.get('x-time-millis');
105
+ if (xTime) {
106
+ data.timeMillis = Number(xTime);
51
107
  }
52
108
  }
53
109
  return data;
@@ -186,12 +242,12 @@ class RingRestClient {
186
242
  const response = requestError.response || {}, responseData = response.body || {}, responseError = 'error' in responseData && typeof responseData.error === 'string'
187
243
  ? responseData.error
188
244
  : '';
189
- if (response.statusCode === 412 || // need 2fa code
190
- (response.statusCode === 400 &&
245
+ if (response.status === 412 || // need 2fa code
246
+ (response.status === 400 &&
191
247
  responseError.startsWith('Verification Code')) // invalid 2fa code entered
192
248
  ) {
193
249
  this.using2fa = true;
194
- if (response.statusCode === 400) {
250
+ if (response.status === 400) {
195
251
  this.promptFor2fa = 'Invalid 2fa code entered. Please try again.';
196
252
  throw new Error(responseError);
197
253
  }
@@ -245,12 +301,12 @@ class RingRestClient {
245
301
  }
246
302
  catch (e) {
247
303
  const response = e.response || {};
248
- if (response.statusCode === 401) {
304
+ if (response.status === 401) {
249
305
  await this.refreshAuth();
250
306
  return this.getSession();
251
307
  }
252
- if (response.statusCode === 429) {
253
- const retryAfter = e.response.headers['retry-after'], waitSeconds = isNaN(retryAfter)
308
+ if (response.status === 429) {
309
+ const retryAfter = e.response.headers.get('retry-after'), waitSeconds = isNaN(retryAfter)
254
310
  ? 200
255
311
  : Number.parseInt(retryAfter, 10);
256
312
  (0, util_1.logError)(`Session response rate limited. Waiting to retry after ${waitSeconds} seconds`);
@@ -297,16 +353,16 @@ class RingRestClient {
297
353
  }
298
354
  catch (e) {
299
355
  const response = e.response || {};
300
- if (response.statusCode === 401) {
356
+ if (response.status === 401) {
301
357
  await this.refreshAuth();
302
358
  return this.request(options);
303
359
  }
304
- if (response.statusCode === 504) {
360
+ if (response.status === 504) {
305
361
  // Gateway Timeout. These should be recoverable, but wait a few seconds just to be on the safe side
306
362
  await (0, util_1.delay)(5000);
307
363
  return this.request(options);
308
364
  }
309
- if (response.statusCode === 404 &&
365
+ if (response.status === 404 &&
310
366
  response.body &&
311
367
  Array.isArray(response.body.errors)) {
312
368
  const errors = response.body.errors, errorText = errors
@@ -320,7 +376,7 @@ class RingRestClient {
320
376
  }
321
377
  (0, util_1.logError)(`http request failed. ${url} returned unknown errors: (${(0, util_1.stringify)(errors)}).`);
322
378
  }
323
- if (response.statusCode === 404 && url.startsWith(clientApiBaseUrl)) {
379
+ if (response.status === 404 && url.startsWith(clientApiBaseUrl)) {
324
380
  (0, util_1.logError)('404 from endpoint ' + url);
325
381
  if (response.body?.error?.includes(hardwareId)) {
326
382
  (0, util_1.logError)('Session hardware_id not found. Creating a new session and trying again.');
@@ -331,8 +387,8 @@ class RingRestClient {
331
387
  }
332
388
  throw new Error('Not found with response: ' + (0, util_1.stringify)(response.body));
333
389
  }
334
- if (response.statusCode) {
335
- (0, util_1.logError)(`Request to ${url} failed with status ${response.statusCode}. Response body: ${(0, util_1.stringify)(response.body)}`);
390
+ if (response.status) {
391
+ (0, util_1.logError)(`Request to ${url} failed with status ${response.status}. Response body: ${(0, util_1.stringify)(response.body)}`);
336
392
  }
337
393
  else if (!options.allowNoResponse) {
338
394
  (0, util_1.logError)(`Request to ${url} failed:`);
@@ -1,4 +1,4 @@
1
- import { CameraData, CameraDeviceSettingsData, CameraEventOptions, CameraEventResponse, CameraHealth, HistoryOptions, PeriodicFootageResponse, PushNotification, PushNotificationDingV2, VideoSearchResponse, OnvifCameraData, RingCameraKind } from './ring-types';
1
+ import { CameraData, CameraDeviceSettingsData, CameraEventOptions, CameraEventResponse, CameraHealth, HistoryOptions, PeriodicFootageResponse, PushNotificationDingV2, VideoSearchResponse, OnvifCameraData, RingCameraKind } from './ring-types';
2
2
  import { RingRestClient } from './rest-client';
3
3
  import { BehaviorSubject, Subject } from 'rxjs';
4
4
  import { DeepPartial } from './util';
@@ -61,7 +61,7 @@ export declare class RingCamera extends Subscribed {
61
61
  private createStreamingConnection;
62
62
  startLiveCall(options?: StreamingConnectionOptions): Promise<StreamingSession>;
63
63
  private removeDingById;
64
- processPushNotification(notification: PushNotification): void;
64
+ processPushNotification(notification: PushNotificationDingV2): void;
65
65
  getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
66
66
  videoSearch({ dateFrom, dateTo, order }?: {
67
67
  dateFrom: number;
@@ -38,7 +38,10 @@ function getBatteryLevel(data) {
38
38
  parseBatteryLife(data.battery_life_2),
39
39
  ].filter((level) => level !== null), { health } = data;
40
40
  if (!levels.length ||
41
- (health && !health.battery_percentage && !health.battery_present)) {
41
+ (health &&
42
+ !health.battery_percentage &&
43
+ !health.battery_present &&
44
+ !health.second_battery_percentage)) {
42
45
  return null;
43
46
  }
44
47
  return Math.min(...levels);
@@ -374,14 +377,13 @@ class RingCamera extends subscribed_1.Subscribed {
374
377
  }
375
378
  async getNextSnapshot({ afterMs, maxWaitMs, force, uuid, }) {
376
379
  const response = await this.restClient.request({
377
- url: `https://app-snaps.ring.com/snapshots/next/${this.id}`,
378
- responseType: 'buffer',
379
- searchParams: {
380
+ url: `https://app-snaps.ring.com/snapshots/next/${this.id}${(0, util_1.buildSearchString)({
380
381
  'after-ms': afterMs,
381
382
  'max-wait-ms': maxWaitMs,
382
383
  extras: force ? 'force' : undefined,
383
384
  uuid: cleanSnapshotUuid(uuid),
384
- },
385
+ })}`,
386
+ responseType: 'buffer',
385
387
  headers: {
386
388
  accept: 'image/jpeg',
387
389
  },
@@ -1,4 +1,4 @@
1
- import { IntercomHandsetAudioData, PushNotification } from './ring-types';
1
+ import { IntercomHandsetAudioData, PushNotificationDingV2, PushNotificationIntercomUnlockV2 } from './ring-types';
2
2
  import { RingRestClient } from './rest-client';
3
3
  import { BehaviorSubject, Subject } from 'rxjs';
4
4
  export declare class RingIntercom {
@@ -22,5 +22,5 @@ export declare class RingIntercom {
22
22
  private doorbotUrl;
23
23
  subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
24
24
  unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
25
- processPushNotification(notification: PushNotification): void;
25
+ processPushNotification(notification: PushNotificationDingV2 | PushNotificationIntercomUnlockV2): void;
26
26
  }
@@ -76,11 +76,13 @@ class RingIntercom {
76
76
  });
77
77
  }
78
78
  processPushNotification(notification) {
79
- if (notification.android_config.category === ring_types_1.PushNotificationAction.Ding) {
79
+ if ('android_config' in notification &&
80
+ notification.android_config.category ===
81
+ ring_types_1.PushNotificationAction.IntercomDing) {
80
82
  this.onDing.next();
81
83
  }
82
- else if (notification.android_config.category ===
83
- ring_types_1.PushNotificationAction.IntercomUnlock) {
84
+ else if ('gcmData' in notification.data &&
85
+ notification.data.gcmData.action === ring_types_1.PushNotificationAction.IntercomUnlock) {
84
86
  this.onUnlocked.next();
85
87
  }
86
88
  }
@@ -469,6 +469,7 @@ export interface BaseCameraData {
469
469
  rssi_category: 'good' | string;
470
470
  battery_voltage_category: 'very_good' | string;
471
471
  second_battery_voltage_category: 'unknown' | string;
472
+ second_battery_percentage?: number;
472
473
  second_battery_percentage_category: 'unknown' | string;
473
474
  battery_save: boolean;
474
475
  firmware_version_status: 'Up to Date';
@@ -867,7 +868,12 @@ export declare enum NotificationDetectionType {
867
868
  export declare enum PushNotificationAction {
868
869
  Ding = "com.ring.pn.live-event.ding",
869
870
  Motion = "com.ring.pn.live-event.motion",
870
- IntercomUnlock = "com.ring.pn.intercom.virtual.unlock"
871
+ IntercomDing = "com.ring.pn.live-event.intercom",
872
+ IntercomUnlock = "com.ring.push.INTERCOM_UNLOCK_FROM_APP",
873
+ AlarmModeNone = "com.ring.push.HANDLE_NEW_SECURITY_PANEL_MODE_NONE_NOTICE",
874
+ AlarmModeSome = "com.ring.push.HANDLE_NEW_SECURITY_PANEL_MODE_SOME_NOTICE",
875
+ AlarmSoundSiren = "com.ring.push.HANDLE_NEW_USER_SOUND_SIREN",
876
+ AlarmSilenceSiren = "com.ring.push.HANDLE_NEW_NON_ALARM_SIREN_SILENCED"
871
877
  }
872
878
  export interface PushNotificationDingV2 {
873
879
  version: '2.0.0' | string;
@@ -888,7 +894,7 @@ export interface PushNotificationDingV2 {
888
894
  device: {
889
895
  e2ee_enabled: boolean;
890
896
  id: number;
891
- kind: RingCameraKind;
897
+ kind: RingCameraKind | RingDeviceType.IntercomHandsetAudio;
892
898
  name: string;
893
899
  };
894
900
  event: {
@@ -896,7 +902,7 @@ export interface PushNotificationDingV2 {
896
902
  id: string;
897
903
  created_at: string;
898
904
  subtype: 'other_motion' | 'motion' | 'ding' | 'human' | string;
899
- detection_type: NotificationDetectionType;
905
+ detection_type?: NotificationDetectionType;
900
906
  };
901
907
  eventito: {
902
908
  type: NotificationDetectionType;
@@ -919,7 +925,7 @@ export interface PushNotificationDingV2 {
919
925
  snapshot_uuid: string;
920
926
  };
921
927
  }
922
- export interface PushNotificationAlarm {
928
+ interface PushNotificationAlarm {
923
929
  aps: {
924
930
  alert: string;
925
931
  };
@@ -929,7 +935,27 @@ export interface PushNotificationAlarm {
929
935
  location_id: string;
930
936
  };
931
937
  }
932
- export type PushNotification = PushNotificationDingV2;
938
+ export interface PushNotificationAlarmV2 {
939
+ data: {
940
+ gcmData: PushNotificationAlarm;
941
+ };
942
+ }
943
+ interface PushNotificationIntercomUnlock {
944
+ aps: {
945
+ alert: string;
946
+ };
947
+ action: PushNotificationAction.IntercomUnlock;
948
+ alarm_meta: {
949
+ device_zid: number;
950
+ location_id: string;
951
+ };
952
+ }
953
+ export interface PushNotificationIntercomUnlockV2 {
954
+ data: {
955
+ gcmData: PushNotificationIntercomUnlock;
956
+ };
957
+ }
958
+ export type PushNotification = PushNotificationDingV2 | PushNotificationAlarmV2 | PushNotificationIntercomUnlockV2;
933
959
  export interface SocketTicketResponse {
934
960
  ticket: string;
935
961
  responseTimestampe: number;
package/lib/ring-types.js CHANGED
@@ -188,7 +188,12 @@ var PushNotificationAction;
188
188
  (function (PushNotificationAction) {
189
189
  PushNotificationAction["Ding"] = "com.ring.pn.live-event.ding";
190
190
  PushNotificationAction["Motion"] = "com.ring.pn.live-event.motion";
191
- PushNotificationAction["IntercomUnlock"] = "com.ring.pn.intercom.virtual.unlock";
191
+ PushNotificationAction["IntercomDing"] = "com.ring.pn.live-event.intercom";
192
+ PushNotificationAction["IntercomUnlock"] = "com.ring.push.INTERCOM_UNLOCK_FROM_APP";
193
+ PushNotificationAction["AlarmModeNone"] = "com.ring.push.HANDLE_NEW_SECURITY_PANEL_MODE_NONE_NOTICE";
194
+ PushNotificationAction["AlarmModeSome"] = "com.ring.push.HANDLE_NEW_SECURITY_PANEL_MODE_SOME_NOTICE";
195
+ PushNotificationAction["AlarmSoundSiren"] = "com.ring.push.HANDLE_NEW_USER_SOUND_SIREN";
196
+ PushNotificationAction["AlarmSilenceSiren"] = "com.ring.push.HANDLE_NEW_NON_ALARM_SIREN_SILENCED";
192
197
  })(PushNotificationAction || (exports.PushNotificationAction = PushNotificationAction = {}));
193
198
  // eslint-disable-next-line no-shadow
194
199
  var DispatchSignalType;
package/lib/util.d.ts CHANGED
@@ -21,4 +21,5 @@ export type DeepPartial<T> = {
21
21
  };
22
22
  export declare function fromBase64(encodedInput: string): string;
23
23
  export declare function toBase64(input: string): string;
24
+ export declare function buildSearchString(search: Record<string, any>): string;
24
25
  export {};
package/lib/util.js CHANGED
@@ -19,6 +19,7 @@ exports.randomInteger = randomInteger;
19
19
  exports.randomString = randomString;
20
20
  exports.fromBase64 = fromBase64;
21
21
  exports.toBase64 = toBase64;
22
+ exports.buildSearchString = buildSearchString;
22
23
  const debug_1 = __importDefault(require("debug"));
23
24
  const colors_1 = require("colors");
24
25
  const readline_1 = require("readline");
@@ -143,3 +144,10 @@ function toBase64(input) {
143
144
  const buff = Buffer.from(input);
144
145
  return buff.toString('base64');
145
146
  }
147
+ function buildSearchString(search) {
148
+ return ('?' +
149
+ Object.entries(search)
150
+ .filter(([, value]) => value !== undefined && value !== null)
151
+ .map(([key, value]) => `${key}=${value}`)
152
+ .join('&'));
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ring-client-api",
3
- "version": "13.0.0-beta.1",
3
+ "version": "13.0.0-beta.3",
4
4
  "description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -17,36 +17,32 @@
17
17
  "test:watch": "jest --watch"
18
18
  },
19
19
  "dependencies": {
20
- "@eneris/push-receiver": "^4.1.2",
20
+ "@eneris/push-receiver": "^4.1.4",
21
21
  "@homebridge/camera-utils": "^2.2.6",
22
- "@peculiar/asn1-schema": "^2.3.8",
23
22
  "@types/socket.io-client": "1.4.36",
24
23
  "colors": "1.4.0",
25
24
  "debug": "^4.3.5",
26
- "got": "^11.8.5",
27
25
  "json-bigint": "^1.0.0",
28
- "msw": "^1.3.3",
29
- "pvtsutils": "^1.3.5",
30
26
  "rxjs": "^7.8.1",
31
27
  "socket.io-client": "^2.5.0",
32
28
  "systeminformation": "^5.22.11",
33
29
  "uuid": "^10.0.0",
34
- "webcrypto-core": "^1.8.0",
35
- "werift": "0.19.3",
36
- "ws": "^8.17.1"
30
+ "werift": "0.19.4",
31
+ "ws": "^8.18.0"
37
32
  },
38
33
  "devDependencies": {
39
34
  "@types/debug": "4.1.12",
40
35
  "@types/jest": "29.5.12",
41
36
  "@types/json-bigint": "^1.0.4",
42
- "@types/node": "20.14.7",
37
+ "@types/node": "20.14.11",
43
38
  "@types/uuid": "10.0.0",
44
- "@types/ws": "^8.5.10",
39
+ "@types/ws": "^8.5.11",
45
40
  "eslint-config-shared": "*",
46
41
  "jest": "29.7.0",
47
- "ts-jest": "29.1.5",
42
+ "msw": "^1.3.3",
43
+ "ts-jest": "29.2.3",
48
44
  "tsconfig": "*",
49
- "typescript": "5.5.2"
45
+ "typescript": "5.5.3"
50
46
  },
51
47
  "engines": {
52
48
  "node": "^18 || ^20"