From bdba75452053791648ebd2e283fd87b637bade06 Mon Sep 17 00:00:00 2001 From: Don Boulia Date: Fri, 4 Dec 2020 14:19:07 -0500 Subject: [PATCH 1/5] fixes for latest blink API --- lib/blink.js | 266 ++++++++++++++++++--------------------- lib/blink_camera.js | 37 +++--- lib/blink_url_handler.js | 5 +- 3 files changed, 136 insertions(+), 172 deletions(-) diff --git a/lib/blink.js b/lib/blink.js index 554937f..8207b61 100644 --- a/lib/blink.js +++ b/lib/blink.js @@ -11,7 +11,7 @@ const { guid } = require('./util'); const request = require('request'); -function _statusCodeIsError(response){ +function _statusCodeIsError(response) { return response.statusCode < 200 || response.statusCode > 299 } module.exports = class Blink { @@ -76,55 +76,60 @@ module.exports = class Blink { for (var id in this._cameras) { if (this._cameras.hasOwnProperty(id)) { let camera = this._cameras[id]; - Object.keys(summaries).forEach(networkId => { - summaries[networkId].devices.forEach(device => { - if (device.device_id === camera.id) { - camera.update(device); - } - }); - }) + + summaries.cameras.forEach(device => { + if (device.id === camera.id) { + camera.update(device); + } + }); } } }); } + filterNetworks(array, index) { + const networks = this.networks.map(_ => _.id); + const result = []; + + array.forEach((el) => { + if (networks.includes(el[index])) { + result.push(el); + } + }); + + return result; + } + getSummary() { const promises = []; const networks = this.networks.map(_ => _.id); - networks.forEach(networkId => { - promises.push(new Promise((resolve, reject) => { - if (!this._auth_header) { - return reject(new BlinkException("Authentication token must be set")); + console.log('included networks: ' + JSON.stringify(networks)); + + return new Promise((resolve, reject) => { + if (!this._auth_header) { + return reject(new BlinkException("Authentication token must be set")); + } + + request({ + url: this.urls.home_url, + headers: this._auth_header, + json: true + }, (err, response, body) => { + + if (err || _statusCodeIsError(response)) { + return reject(new BlinkException(`Can't retrieve system summary`)); } - request({ - url: this.urls.network_url + networkId + "/homescreen", - headers: this._auth_header, - json: true - }, (err, response, body) => { - if (err || _statusCodeIsError(response)) { - return reject(new BlinkException(`Can't retrieve system summary`)); - } - return resolve(body); - }); - })); - }); + // filter based on networks that were selected - return Promise.all(promises) - .then(results => { - return results.reduce((acc, result, index) => { - acc[networks[index]] = result; - this._networks.forEach(network => { - if (network.id !== networks[index]) return; - network = { - ...network, - ...result, - }; - }); - return acc; - }, {}); + body.networks = this.filterNetworks(body.networks, 'id'); + body.sync_modules = this.filterNetworks(body.sync_modules, 'network_id'); + body.cameras = this.filterNetworks(body.cameras, 'network_id'); + + return resolve(body); }); + }); } getCameraThumbs() { @@ -133,93 +138,37 @@ module.exports = class Blink { var result = {}; for (var id in this._cameras) { if (this._cameras.hasOwnProperty(id)) { - result[id] = this._cameras.thumbnail; + result[id] = this._cameras[id].thumbnail; } } return result; }); } - getEvents(networkIds = []) { - const promises = []; - const networks = networkIds.length ? networkIds : this.networks.map(_ => _.id); - - networks.forEach(networkId => { - promises.push(new Promise((resolve, reject) => { - request({ - url: this.urls.event_url + networkId, - headers: this._auth_header, - json: true - }, (err, response, body) => { - if (err || _statusCodeIsError(response)) { - return reject(new BlinkException(`Can't retrieve system events`)); - } - this._events = body.event; - return resolve(this._events); - }); - })); - }); - - return Promise.all(promises) - .then(results => { - return results.reduce((acc, result, index) => { - acc[networks[index]] = result; - return acc; - }, {}); - }); - } - isOnline(networkIds = []) { - const promises = []; const networks = networkIds.length ? networkIds : this.networks.map(_ => _.id); - networks.forEach(networkId => { - promises.push(new Promise((resolve, reject) => { - request({ - url: this.urls.network_url + networkId + '/syncmodules', - headers: this._auth_header, - json: true - }, (err, response, body) => { - if (err || _statusCodeIsError(response)) { - return reject(new BlinkException(`Can't retrieve system status`)); - } - return resolve(body.syncmodule.status === 'online'); - }); - })); - }); - - return Promise.all(promises) - .then(results => { - return results.reduce((acc, result, index) => { - acc[networks[index]] = result; - return acc; - }, {}); - }); - } + return this.getSummary() + .then(summaries => { - getClients() { - return new Promise((resolve, reject) => { - request({ - url: this.urls.base_url + '/account/clients', - headers: this._auth_header, - json: true - }, (err, response, body) => { - if (err || _statusCodeIsError(response)) { - return reject(new BlinkException(`Can't retrieve connected clients`)); + const result = {}; + summaries.sync_modules.forEach((el) => { + if (networks.includes(el.network_id)) { + result[el.network_id] = (el.status === "online") ? true : false ; } - return resolve(body); }); + + return result; }); } getLastMotions() { - return this.getEvents() + return this.getVideos() .then(events => { var result = {}; events.forEach(event => { let camera_id = event.camera_id; - let camera = this._cameras[camera_id]; - + let camera = this._cameras[camera_id];sssssssssssss if (event.type === 'motion') { let url = this.urls.base_url + event.video_url; result[camera_id] = camera.motion = { @@ -236,10 +185,15 @@ module.exports = class Blink { isArmed() { return this.getSummary() .then(summaries => { - return Object.keys(summaries).reduce((acc, key) => { - acc[key] = summaries[key].network.armed; - return acc; - }, {}); + + const networks = this.networks.map(_ => _.id); + + const result = {}; + summaries.networks.forEach((el) => { + result[el.id] = el.armed; + }); + + return result; }); } @@ -250,8 +204,9 @@ module.exports = class Blink { networksToArm.forEach(networkId => { promises.push(new Promise((resolve, reject) => { let state = value ? 'arm' : 'disarm'; + console.log('arm url: ' + this.urls.arm_url + networkId + '/state/' + state); request.post({ - url: this.urls.network_url + networkId + '/' + state, + url: this.urls.arm_url + networkId + '/state/' + state, json: true, headers: this._auth_header, body: {} @@ -282,7 +237,7 @@ module.exports = class Blink { }, (err, response, body) => { if (err || _statusCodeIsError(response)) { return reject(new BlinkException(`Can't fetch videos`)); - } + } return resolve(body); }); }); @@ -291,17 +246,16 @@ module.exports = class Blink { getCameras() { return this.getSummary() .then(summaries => { - Object.keys(summaries).forEach(networkId => { - summaries[networkId].devices.forEach(device => { - if (device.device_type === 'camera') { - device.region_id = this._region_id; - device.network_id = networkId; - let newDevice = new BlinkCamera(device, this.urls); - this._cameras[newDevice.id] = newDevice; - this._idlookup[newDevice.id] = newDevice.name; - } - }); + + console.log('getCameras: ' + JSON.stringify(summaries.cameras)); + summaries.cameras.forEach(camera => { + camera.region_id = this._region_id; + + const newDevice = new BlinkCamera(camera, this.urls); + this._cameras[newDevice.id] = newDevice; + this._idlookup[newDevice.id] = newDevice.name; }); + return this._cameras; }); } @@ -315,35 +269,38 @@ module.exports = class Blink { let arm_url = network_id_url + '/camera/' + camera.id + '/'; camera.image_link = image_url; camera.arm_link = arm_url; + console.log("setting camera header " + this._auth_header); camera.header = this._auth_header; } } } setupSystem(name_or_id) { - if ( !((this._username && this._password) || (this._token && this._region_id ))) { + if (!((this._username && this._password) || (this._token && this._region_id))) { throw new BlinkAuthenticationException("(_username, _password) or (_token, _region_id) are required for system setup"); } - if(this._token){ + if (this._token) { this._setupWithToken() return this.getIDs(name_or_id) .then(this.getCameras.bind(this)) .then(this.getLinks.bind(this)); } - else{ + else { return this._getAuthToken() .then(this.getIDs.bind(this, name_or_id)) .then(this.getCameras.bind(this)) .then(this.getLinks.bind(this)); } } - _setupWithToken(){ - this._host = this._region_id + '.' + BLINK_URL; - this._auth_header = { - 'TOKEN_AUTH': this._token - }; - this.urls = new BlinkURLHandler(this._account_id, this._region_id); + + _setupWithToken() { + this._host = 'rest-' + this._region_id + '.' + BLINK_URL; + this._auth_header = { + 'token-auth': this._token + }; + this.urls = new BlinkURLHandler(this._account_id, this._region_id); } + _getAuthToken(repeats = 0) { return new Promise((resolve, reject) => { if (typeof this._username != 'string') { @@ -375,14 +332,14 @@ module.exports = class Blink { }; let authenticate = (response) => { - this._host = this._region_id + '.' + BLINK_URL; - this._token = response.authtoken.authtoken; + this._host = 'rest-' + this._region_id + '.' + BLINK_URL; this._auth_header = { - 'TOKEN_AUTH': this._token + 'token-auth': this._token }; this.urls = new BlinkURLHandler(this._account_id, this._region_id); + resolve(true); }; @@ -399,7 +356,7 @@ module.exports = class Blink { if (!this.auth_2FA) { if (repeats === 1) return reject(new BlinkAuthenticationException(`Authentication problem: verification timeout`)); return new Promise((resolve, reject) => { - setTimeout(() => { + setTimeout(() => { this._getAuthToken(repeats + 1).then(resolve, reject); }, this.verification_timeout); }).then(resolve, reject); @@ -409,24 +366,34 @@ module.exports = class Blink { output: process.stdout }); return rl.question(`Enter the verification code sent to ${this._username}: `, pin => { + this._region_id = body.region.tier; + this._region = body.region.code; + this._token = body.authtoken.authtoken; + + const urls = new BlinkURLHandler(body.account.id, body.region.tier); + headers['token-auth'] = body.authtoken.authtoken; + + console.log(`url: ${urls.base_url}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`); + console.log('headers: ' + JSON.stringify(headers)); + console.log('pin: ' + pin); + request.post({ - url: `${BASE_URL}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`, + url: `${urls.base_url}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`, json: true, headers: headers, body: { pin: `${pin}`, } }, (err, response, body) => { + console.log('response: ' + JSON.stringify(body)); + if (err || _statusCodeIsError(response)) { return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); } - if (!body.region) { + if (!valid) { return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); } - for (var key in body.region) { - this._region_id = key; - this._region = body.region[key]; - } + authenticate(body); }); rl.close(); @@ -435,10 +402,13 @@ module.exports = class Blink { if (!body.region) { return reject(new BlinkAuthenticationException(body.message)); } - for (var key in body.region) { - this._region_id = key; - this._region = body.region[key]; - } + + console.log('body: ' + JSON.stringify(body)); + + this._region_id = body.region.tier; + this._region = body.region.code; + this._token = body.authtoken.authtoken; + authenticate(body); } }); @@ -452,7 +422,7 @@ module.exports = class Blink { return reject(new BlinkException("You have to be authenticated before calling this method")); } request({ - url: this.urls.networks_url, + url: this.urls.home_url, headers: this._auth_header, json: true }, (err, response, body) => { @@ -461,7 +431,7 @@ module.exports = class Blink { } else { var network = false; if (typeof name_or_id != 'undefined') { - body.networks.forEach(function(n){ + body.networks.forEach(function (n) { if (n.id == name_or_id || n.name == name_or_id) { network = n; that._networks.push(network); @@ -480,8 +450,10 @@ module.exports = class Blink { }); } - that._account_id = that._networks[0].account_id; - that.urls = new BlinkURLHandler(that.accountId, that.regionId); + console.log('account: ' + JSON.stringify(body.account)); + + that._account_id = body.account.id; + that.urls = new BlinkURLHandler(that._account_id, that.regionId); return resolve(that); } }); diff --git a/lib/blink_camera.js b/lib/blink_camera.js index b771832..c3b0e12 100644 --- a/lib/blink_camera.js +++ b/lib/blink_camera.js @@ -8,23 +8,22 @@ const request = require('request'); module.exports = class BlinkCamera { constructor(config, urls) { this.urls = urls; - this._id = config.device_id; + this._id = config.id; this._name = config.name; - this._status = config.armed; + this._status = config.status; this._enabled = config.enabled; this._thumb = this.urls.base_url + config.thumbnail + '.jpg'; this._clip = this.urls.base_url + config.thumbnail + '.mp4'; - this._temperature = config.temp; + this._temperature = config.signals.temp; this._battery = config.battery; - this._notifications = config.notifications; this._motion = {}; this._header = null; this._image_link = null; this._arm_link = null; this._updated_at = config.updated_at; this._region_id = config.region_id; - this._wifi = null; - this._lfr = null; + this._wifi = config.signals.wifi; + this._lfr = config.signals.lfr; this._network_id = config.network_id; } @@ -92,14 +91,6 @@ module.exports = class BlinkCamera { this._battery = value; } - get notifications() { - return this._notifications; - } - - set notifications(value) { - this._notifications = value; - } - get image_link() { return this._image_link; } @@ -180,13 +171,12 @@ module.exports = class BlinkCamera { update(values) { this._name = values['name']; - this._status = values['armed']; + this._status = values['status']; this._enabled = values['enabled']; this._thumb = this.urls.base_url + values['thumbnail'] + '.jpg'; this._clip = this.urls.base_url + values['thumbnail'] + '.mp4'; - this._temperature = values['temp']; + this._temperature = values['signals'].temp; this._battery = values['battery']; - this._notifications = values['notifications']; this._updated_at = values['updated_at']; } @@ -200,11 +190,11 @@ module.exports = class BlinkCamera { if (err) { reject(new BlinkException(`Can't refresh thumbnail for camera ${this._id}:${this._name}`)); } else { - let devices = body.devices; - devices.forEach((device) => { - if (device.device_id === this._id) { - this._thumb = this.urls.base_url + device['thumbnail'] + '.jpg'; - this._updated_at = device['updated_at']; + let cameras = body.cameras; + cameras.forEach((camera) => { + if (camera.id === this._id) { + this._thumb = this.urls.base_url + camera['thumbnail'] + '.jpg'; + this._updated_at = camera['updated_at']; resolve(this._thumb); } }); @@ -234,6 +224,9 @@ module.exports = class BlinkCamera { fetchImageData() { return new Promise((resolve, reject) => { + console.log('thumbnail ' + this.thumbnail); + console.log('header ' + this._header); + request({ url: this.thumbnail, headers: this._header, diff --git a/lib/blink_url_handler.js b/lib/blink_url_handler.js index c0eb812..8015d79 100644 --- a/lib/blink_url_handler.js +++ b/lib/blink_url_handler.js @@ -5,10 +5,9 @@ module.exports = class BlinkURLHandler { constructor(account_id, region_id) { this.base_url = 'https://rest-' + region_id + '.' + BLINK_URL; - this.event_url = this.base_url + '/events/network/'; this.network_url = this.base_url + '/network/'; - this.networks_url = this.base_url + '/networks'; + this.arm_url = this.base_url + `/api/v1/accounts/${account_id}/networks/`; this.video_url = this.base_url + `/api/v1/accounts/${account_id}/media/changed`; - this.home_url = this.base_url + '/homescreen'; + this.home_url = this.base_url + '/api/v3/accounts/' + account_id + '/homescreen'; } }; From 3a87e4bee9def1c7fe3d137f6cb1846c8171306a Mon Sep 17 00:00:00 2001 From: Don Boulia Date: Tue, 29 Dec 2020 09:16:59 -0500 Subject: [PATCH 2/5] comment out debug messages --- lib/blink.js | 20 ++++++++++---------- lib/blink_camera.js | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/blink.js b/lib/blink.js index 8207b61..65d8996 100644 --- a/lib/blink.js +++ b/lib/blink.js @@ -104,7 +104,7 @@ module.exports = class Blink { const promises = []; const networks = this.networks.map(_ => _.id); - console.log('included networks: ' + JSON.stringify(networks)); + // console.log('included networks: ' + JSON.stringify(networks)); return new Promise((resolve, reject) => { if (!this._auth_header) { @@ -204,7 +204,7 @@ module.exports = class Blink { networksToArm.forEach(networkId => { promises.push(new Promise((resolve, reject) => { let state = value ? 'arm' : 'disarm'; - console.log('arm url: ' + this.urls.arm_url + networkId + '/state/' + state); + // console.log('arm url: ' + this.urls.arm_url + networkId + '/state/' + state); request.post({ url: this.urls.arm_url + networkId + '/state/' + state, json: true, @@ -247,7 +247,7 @@ module.exports = class Blink { return this.getSummary() .then(summaries => { - console.log('getCameras: ' + JSON.stringify(summaries.cameras)); + // console.log('getCameras: ' + JSON.stringify(summaries.cameras)); summaries.cameras.forEach(camera => { camera.region_id = this._region_id; @@ -269,7 +269,7 @@ module.exports = class Blink { let arm_url = network_id_url + '/camera/' + camera.id + '/'; camera.image_link = image_url; camera.arm_link = arm_url; - console.log("setting camera header " + this._auth_header); + // console.log("setting camera header " + this._auth_header); camera.header = this._auth_header; } } @@ -373,9 +373,9 @@ module.exports = class Blink { const urls = new BlinkURLHandler(body.account.id, body.region.tier); headers['token-auth'] = body.authtoken.authtoken; - console.log(`url: ${urls.base_url}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`); - console.log('headers: ' + JSON.stringify(headers)); - console.log('pin: ' + pin); + // console.log(`url: ${urls.base_url}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`); + // console.log('headers: ' + JSON.stringify(headers)); + // console.log('pin: ' + pin); request.post({ url: `${urls.base_url}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`, @@ -385,7 +385,7 @@ module.exports = class Blink { pin: `${pin}`, } }, (err, response, body) => { - console.log('response: ' + JSON.stringify(body)); + // console.log('response: ' + JSON.stringify(body)); if (err || _statusCodeIsError(response)) { return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); @@ -403,7 +403,7 @@ module.exports = class Blink { return reject(new BlinkAuthenticationException(body.message)); } - console.log('body: ' + JSON.stringify(body)); + // console.log('body: ' + JSON.stringify(body)); this._region_id = body.region.tier; this._region = body.region.code; @@ -450,7 +450,7 @@ module.exports = class Blink { }); } - console.log('account: ' + JSON.stringify(body.account)); + // console.log('account: ' + JSON.stringify(body.account)); that._account_id = body.account.id; that.urls = new BlinkURLHandler(that._account_id, that.regionId); diff --git a/lib/blink_camera.js b/lib/blink_camera.js index c3b0e12..7d4176d 100644 --- a/lib/blink_camera.js +++ b/lib/blink_camera.js @@ -224,8 +224,8 @@ module.exports = class BlinkCamera { fetchImageData() { return new Promise((resolve, reject) => { - console.log('thumbnail ' + this.thumbnail); - console.log('header ' + this._header); + // console.log('thumbnail ' + this.thumbnail); + // console.log('header ' + this._header); request({ url: this.thumbnail, From 9f8f99b231438f563690c3fd9d2f781abaa357c7 Mon Sep 17 00:00:00 2001 From: Don Boulia Date: Sun, 28 Mar 2021 17:06:59 -0400 Subject: [PATCH 3/5] changes for v5 login --- lib/blink.js | 27 ++++++++++++++------------- lib/constants.js | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/blink.js b/lib/blink.js index 65d8996..4075cc1 100644 --- a/lib/blink.js +++ b/lib/blink.js @@ -322,7 +322,7 @@ module.exports = class Blink { "password": this._password, "notification_key": notification_key, "unique_id": this._device_id, - "app_version": "6.0.7 (520300) #afb0be72a", + "app_version": "6.0.12", "client_name": this.device_name, "client_type": "android", "device_identifier": this._device_id, @@ -352,7 +352,7 @@ module.exports = class Blink { if (err || _statusCodeIsError(response)) { return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); } else { - if ((body.client || {}).verification_required) { + if ((body.account || {}).account_verification_required) { if (!this.auth_2FA) { if (repeats === 1) return reject(new BlinkAuthenticationException(`Authentication problem: verification timeout`)); return new Promise((resolve, reject) => { @@ -366,19 +366,19 @@ module.exports = class Blink { output: process.stdout }); return rl.question(`Enter the verification code sent to ${this._username}: `, pin => { - this._region_id = body.region.tier; - this._region = body.region.code; - this._token = body.authtoken.authtoken; + this._region_id = body.account.tier; + this._region = body.account.region; + this._token = body.auth.token; - const urls = new BlinkURLHandler(body.account.id, body.region.tier); - headers['token-auth'] = body.authtoken.authtoken; + const urls = new BlinkURLHandler(body.account.account_id, body.account.tier); + headers['token-auth'] = body.auth.token; - // console.log(`url: ${urls.base_url}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`); + // console.log(`url: ${urls.base_url}/api/v4/account/${body.account.account_id}/client/${body.account.client_id}/pin/verify`); // console.log('headers: ' + JSON.stringify(headers)); // console.log('pin: ' + pin); request.post({ - url: `${urls.base_url}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`, + url: `${urls.base_url}/api/v4/account/${body.account.account_id}/client/${body.acount.client_id}/pin/verify`, json: true, headers: headers, body: { @@ -399,15 +399,16 @@ module.exports = class Blink { rl.close(); }); } - if (!body.region) { + if (!body.account.region) { + console.log('body ', body); return reject(new BlinkAuthenticationException(body.message)); } // console.log('body: ' + JSON.stringify(body)); - this._region_id = body.region.tier; - this._region = body.region.code; - this._token = body.authtoken.authtoken; + this._region_id = body.account.tier; + this._region = body.account.region; + this._token = body.auth.token; authenticate(body); } diff --git a/lib/constants.js b/lib/constants.js index 521ac55..92e9d89 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -4,7 +4,7 @@ global.BLINK_URL = 'immedia-semi.com'; global.LOGIN_URL = 'https://rest-prod.' + BLINK_URL + '/api/v3/login'; -global.LOGIN_URL_2FA = 'https://rest-prod.' + BLINK_URL + '/api/v4/account/login'; +global.LOGIN_URL_2FA = 'https://rest-prod.' + BLINK_URL + '/api/v5/account/login'; global.BASE_URL = 'https://rest-prod.' + BLINK_URL; global.DEFAULT_URL = 'prod.' + BLINK_URL; global.SIZE_NOTIFICATION_KEY = 152; From 5a7afbefc32a818d3e7317a9405bf5b8590ff9f7 Mon Sep 17 00:00:00 2001 From: Don Boulia Date: Sat, 12 Jun 2021 08:04:08 -0400 Subject: [PATCH 4/5] Set account_id from response body --- lib/blink.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/blink.js b/lib/blink.js index 4075cc1..858fa7c 100644 --- a/lib/blink.js +++ b/lib/blink.js @@ -332,6 +332,8 @@ module.exports = class Blink { }; let authenticate = (response) => { + // console.log('auth response: ', response); + this._host = 'rest-' + this._region_id + '.' + BLINK_URL; this._auth_header = { @@ -369,7 +371,9 @@ module.exports = class Blink { this._region_id = body.account.tier; this._region = body.account.region; this._token = body.auth.token; - + this._account_id = body.account.account_id; + + console.log('body.account.account_id', body.account.account_id); const urls = new BlinkURLHandler(body.account.account_id, body.account.tier); headers['token-auth'] = body.auth.token; @@ -409,6 +413,7 @@ module.exports = class Blink { this._region_id = body.account.tier; this._region = body.account.region; this._token = body.auth.token; + this._account_id = body.account.account_id; authenticate(body); } @@ -428,6 +433,7 @@ module.exports = class Blink { json: true }, (err, response, body) => { if (err || _statusCodeIsError(response)) { + console.log('error ', body); return reject(new BlinkException(`Can't retrieve system status`)); } else { var network = false; From 2338cc3159fb0906ba38a1dd1c2a44b35f9bc723 Mon Sep 17 00:00:00 2001 From: Don Boulia Date: Mon, 24 Nov 2025 12:28:37 -0500 Subject: [PATCH 5/5] changes to implement latest Blink authentication --- README.md | 95 +++++++++-- index.js | 3 +- lib/api.js | 171 +++++++++++++++++++ lib/blink.js | 256 +++++++--------------------- lib/blink_2fa_exception.js | 7 + lib/blink_auth.js | 331 +++++++++++++++++++++++++++++++++++++ lib/blink_camera.js | 131 +++++---------- lib/constants.js | 19 +++ lib/logger.js | 17 ++ lib/util.js | 57 ++++++- package.json | 6 +- 11 files changed, 787 insertions(+), 306 deletions(-) create mode 100644 lib/api.js create mode 100644 lib/blink_2fa_exception.js create mode 100644 lib/blink_auth.js create mode 100644 lib/logger.js diff --git a/README.md b/README.md index 38f3f04..c1fe32a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![License](https://img.shields.io/npm/l/node-blink-security.svg) # node-blink-security -This is a Node.js version of [this python library](https://github.com/fronzbot/blinkpy). It allows to communicate with Blink Home Security System from a Node.js application. +This is a Node.js version of [this python library](https://github.com/fronzbot/blinkpy). It allows communication with Blink Home Security System from a Node.js application. # Installation ``` @@ -16,36 +16,109 @@ npm install node-blink-security # Usage +Logging in with a valid `username` and `password` will generate a 2FA verification text or email from Blink. You will be prompted via the command line for the 2FA code. Once successfully verified, you will receive an OAuth token and refresh token information that can be stored and reused for future Blink access to avoid needing to re-verify each time. + ```javascript -const Blink = require('node-blink-security'); +const fs = require("fs"); +const {BlinkAuth, Blink} = require('node-blink-security'); + +const blinkAuth = new BlinkAuth({username: 'YOUR_EMAIL', password: 'YOUR_PASSWORD', deviceId:'DEVICE_ID'}); +const blink = new Blink(blinkAuth); -var blink = new Blink('YOUR_EMAIL', 'YOUR_PASSWORD', 'DEVICE_ID'); +const blink = new Blink(); blink.setupSystem() .then(() => { + // save login creds for next time + const loginAttributes = blinkAuth.getLoginAttributes(); + console.log('new tokens', loginAttributes); + + const jsonString = JSON.stringify(loginAttributes, null, 2); + fs.writeFile('creds.json', jsonString, (err) => { + if (err) { + console.error('Error writing JSON file:', err); + } else { + console.log('JSON data successfully written to creds.json'); + } + }); + blink.setArmed() .then(() => { // see the object dump for details - console.log(blink); + console.log('blink'); }); }, (error) => { console.log(error); }); ``` +Previously stored login credentials from `getLoginCredentials` can be reused like this: + +```javascript +const {BlinkAuth, Blink} = require('node-blink-security'); +const fs = require("fs"); +const creds = require("./creds.json"); + +const blinkAuth = new BlinkAuth(creds, true /* noPrompt */); +const blink = new Blink(blinkAuth); + +blink.setupSystem("Cary Outside").then( + () => { + blink.setArmed() + .then(() => { + // see the object dump for details + console.log(blink); + }); + }, + (error) => { + console.log(error); + } +); +``` + +Tokens expire after 4 hours. The `BlinkAuth` will automatically renew the access token when it expires. You can provide a callback to `BlinkAuth` to be notified when tokens are refreshed and store the new tokens like so: + +```javascript +const {BlinkAuth, Blink} = require('node-blink-security'); +const fs = require("fs"); +const creds = require("./creds.json"); + +const blinkAuth = new BlinkAuth(creds, true, callback= () => { + // callback when tokens are refreshed + const loginAttributes = blinkAuth.getLoginAttributes(); + console.log('refreshed tokens', loginAttributes); + + const jsonString = JSON.stringify(loginAttributes, null, 2); + fs.writeFile('creds.json', jsonString, (err) => { + if (err) { + console.error('Error writing JSON file:', err); + } else { + console.log('JSON data successfully written to creds.json'); + } + }); +}); +``` + # API ```javascript -class Blink +class BlinkAuth(loginData, noPrompt=false, callback=null) ``` -## Constructor -* `email` - your Blink account email +Holds authentication information for the Blink account. If no `loginData` is provided, you will be prompted for `username` and `password`. If `username` and `password` are provided, you will be prompted for a 2FA code. If you supply the results of `getLoginAttributes()` from a prior session and specify `noPrompt=true`, the prior authentication information will be used for accessing the Blink API. Finally, if you provide a `callback` function, it will be called whenever the login attributes are refreshed, allowing you to store the new tokens for future use. + +## loginData + +* `username` - your Blink account email * `password` - your Blink account password * `deviceId` - identifies your device and registers it in your account. It's required since version 4.0.0 of this package as this is when Blink switched to 2-factor authentication flow. The value is provided by you and it should let you identify the device correctly when you receive a verification email from Blink. -* `options` -* * `auth_2FA: false` - set to `true` if you want to receive verification code for each login, otherwise you'll receive verification email only once for the first time and after that the device will be remembered by Blink. -* * `verification_timeout: 60000` - number of milliseconds to wait for email verification until retrying account login -* * `device_name: "node-blink-security"` - this name appears in verification email along with your `deviceId` + +## Properties + +* `blinkAuth.getLoginAttributes` - returns the current login attributes (token, refresh token, token expiration, etc.) that can be stored for future use + +```javascript +class Blink +``` ## Properties diff --git a/index.js b/index.js index 075ef57..3a5cd39 100644 --- a/index.js +++ b/index.js @@ -3,5 +3,6 @@ */ const Blink = require('./lib/blink'); +const BlinkAuth = require('./lib/blink_auth'); -module.exports = Blink; \ No newline at end of file +module.exports = { Blink, BlinkAuth }; \ No newline at end of file diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..b398d57 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,171 @@ + +const Logger = require("./logger"); + +/* auth API functions */ +const requestLogin = ( + auth, + url, + loginData, + isRefresh +) => { + const headers = { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": DEFAULT_USER_AGENT, + hardware_id: loginData.deviceId || "Blink-Home", + }; + + // Add 2FA code to headers if provided + if ("twoFACode" in loginData) { + headers["2fa-code"] = loginData.twoFACode; + } + + // Prepare form data for OAuth + const formData = { + username: loginData.username, + client_id: OAUTH_CLIENT_ID, + scope: OAUTH_SCOPE, + }; + + if (isRefresh) { + formData.grant_type = OAUTH_GRANT_TYPE_REFRESH_TOKEN; + formData.refresh_token = auth.refreshToken; + } else { + formData.grant_type = OAUTH_GRANT_TYPE_PASSWORD; + formData.password = loginData.password; + } + + const formParams = new URLSearchParams(formData); + const data = formParams.toString(); + + return auth.post( + url, + headers, + data, + false, // not json + true // skipRefreshCheck + ); +}; + +const requestTier = (auth, url) => { + const loginData = auth.getLoginAttributes(); + + const headers = { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": DEFAULT_USER_AGENT, + Authorization: `Bearer ${loginData.token}`, + }; + + return auth.get(url, headers, false, true); +}; + +/* blink API functions */ +const getIDs = (blink) => { + return httpGet(blink, blink.urls.home_url,true) +} + +const getSummary = (blink) => { + return httpGet(blink, blink.urls.home_url, true); +} + +const setArmed = (blink, networkId, armed) => { + let state = armed ? 'arm' : 'disarm'; + const url = blink.urls.arm_url + networkId + '/state/' + state; + + return httpPost(blink, url, {}, true); +} + +const getVideos = (blink, since, page) => { + const url = `${blink.urls.video_url}?since=${since.toISOString()}&page=${page}` + return httpGet(blink, url, true); +} + +const imageRefresh = (blink) => { + return httpGet(blink, blink.urls.home_url, true); +} + +/* camera API functions */ +const snapPicture = (camera) => { + const blink = camera.blink; + const url = camera.image_link; + return httpPost(blink, url, {}, true); +}; + +const setMotionDetect = (camera, enable) => { + const blink = camera.blink; + const url = camera.arm_link + (enable ? 'enable' : 'disable') + return httpPost(blink, url, {}, true); +} + +const statusRefresh = (camera) => { + const blink = camera.blink; + const url = camera.arm_link + 'status'; + return httpPost(blink, url, {}, true); +} + +const fetchImageData = (camera) => { + const blink = camera.blink; + const thumbnailUrl = camera.thumbnail; + return httpGet(blink, thumbnailUrl, false, true /* binary */); +} + +const fetchVideoData = (camera) => { + const blink = camera.blink; + const clipUrl = camera.clip; + return httpGet(blink, clipUrl, false, true /* binary */); +} + +const recordClip = (camera) => { + const blink = camera.blink; + const url = camera.arm_link + 'clip'; + return httpPost(blink, url, {}, true); +} + +/* HTTP helper functions */ +const httpGet = ( + blink, + url, + json = true, + binary=false +) => { + Logger.debug(`Making GET request to ${url}`); + + return blink.auth.get( + url, + blink.auth.getHeaders(), + json, + false, // skipRefreshCheck + binary + ); +}; + +const httpPost = ( + blink, + url, + data = null, + json = true +) => { + Logger.debug(`Making POST request to ${url}`); + + return blink.auth.post( + url, + blink.auth.getHeaders(), + data, + json + ); +}; + +module.exports = { + requestLogin, + requestTier, + getIDs, + getSummary, + snapPicture, + setMotionDetect, + imageRefresh, + statusRefresh, + fetchImageData, + fetchVideoData, + recordClip, + setArmed, + getVideos +}; \ No newline at end of file diff --git a/lib/blink.js b/lib/blink.js index 858fa7c..62a0afe 100644 --- a/lib/blink.js +++ b/lib/blink.js @@ -6,20 +6,18 @@ require('./constants'); const BlinkCamera = require('./blink_camera'); const BlinkException = require('./blink_exception'); const BlinkAuthenticationException = require('./blink_auth_exception'); +const BlinkTwoFARequiredError = require('./blink_2fa_exception'); const BlinkURLHandler = require('./blink_url_handler'); -const { guid } = require('./util'); - -const request = require('request'); +const util = require('./util'); +const Logger = require('./logger'); +const api = require('./api'); function _statusCodeIsError(response) { return response.statusCode < 200 || response.statusCode > 299 } module.exports = class Blink { - constructor(username, password, device_id, options) { - this._username = username; - this._password = password; - this._token = null; - this._auth_header = null; + constructor(blinkAuth, options) { + this._auth = blinkAuth; this._networks = []; this._account_id = null; this._region = null; @@ -28,10 +26,6 @@ module.exports = class Blink { this._events = []; this._cameras = {}; this._idlookup = {}; - this._device_id = device_id; - this.auth_2FA = false; - this.verification_timeout = 1e3 * 60; - this.device_name = "node-blink-security"; this.urls = null; Object.assign(this, options); } @@ -60,6 +54,10 @@ module.exports = class Blink { return this._region_id; } + get auth() { + return this._auth; + } + refresh() { var promises = []; for (var id in this._cameras) { @@ -101,23 +99,18 @@ module.exports = class Blink { } getSummary() { - const promises = []; - const networks = this.networks.map(_ => _.id); - + // const networks = this.networks.map(_ => _.id); // console.log('included networks: ' + JSON.stringify(networks)); return new Promise((resolve, reject) => { - if (!this._auth_header) { + if (!this.auth.hasToken()) { return reject(new BlinkException("Authentication token must be set")); } - request({ - url: this.urls.home_url, - headers: this._auth_header, - json: true - }, (err, response, body) => { + api.getSummary(this).then((response) => { + const body = response.body; - if (err || _statusCodeIsError(response)) { + if ( _statusCodeIsError(response.status)) { return reject(new BlinkException(`Can't retrieve system summary`)); } @@ -203,19 +196,11 @@ module.exports = class Blink { networksToArm.forEach(networkId => { promises.push(new Promise((resolve, reject) => { - let state = value ? 'arm' : 'disarm'; - // console.log('arm url: ' + this.urls.arm_url + networkId + '/state/' + state); - request.post({ - url: this.urls.arm_url + networkId + '/state/' + state, - json: true, - headers: this._auth_header, - body: {} - }, (err, response, body) => { - if (err || _statusCodeIsError(response)) { - return reject(new BlinkException(`Can't ${state} the network: ${networkId}`)); - } - return resolve(body); - }); + api.setArmed(this, networkId, value).then((response) => { + return resolve(response.body); + }).catch(() => { + return reject(new BlinkException(`Can't ${value ? 'arm' : 'disarm'} the network: ${networkId}`)); + }) })); }); @@ -230,16 +215,11 @@ module.exports = class Blink { getVideos(page = 0, since = new Date(2008)) { // Blink was founded in 2009 return new Promise((resolve, reject) => { - request({ - url: `${this.urls.video_url}?since=${since.toISOString()}&page=${page}`, - headers: this._auth_header, - json: true - }, (err, response, body) => { - if (err || _statusCodeIsError(response)) { + api.getVideos(this, since, page).then((response) => { + return resolve(response.body); + }).catch(() => { return reject(new BlinkException(`Can't fetch videos`)); - } - return resolve(body); - }); + }) }); } @@ -251,7 +231,7 @@ module.exports = class Blink { summaries.cameras.forEach(camera => { camera.region_id = this._region_id; - const newDevice = new BlinkCamera(camera, this.urls); + const newDevice = new BlinkCamera(this, camera, this.urls); this._cameras[newDevice.id] = newDevice; this._idlookup[newDevice.id] = newDevice.name; }); @@ -275,165 +255,60 @@ module.exports = class Blink { } } - setupSystem(name_or_id) { - if (!((this._username && this._password) || (this._token && this._region_id))) { - throw new BlinkAuthenticationException("(_username, _password) or (_token, _region_id) are required for system setup"); + setupUrls() { + try { + this.urls = new BlinkURLHandler(this.auth.accountId, this.auth.regionId); + } catch (e) { + Logger.error( + `Unable to extract region is from response ${this.auth.tierInfo}` + ); + + throw new BlinkSetupError("Blink setup error!"); } - if (this._token) { - this._setupWithToken() + } + + prompt2fa() { + return this.auth.prompt2fa(); + } + + initialize(name_or_id) { + this.setupUrls(); return this.getIDs(name_or_id) .then(this.getCameras.bind(this)) .then(this.getLinks.bind(this)); - } - else { - return this._getAuthToken() - .then(this.getIDs.bind(this, name_or_id)) - .then(this.getCameras.bind(this)) - .then(this.getLinks.bind(this)); - } } - _setupWithToken() { - this._host = 'rest-' + this._region_id + '.' + BLINK_URL; - this._auth_header = { - 'token-auth': this._token - }; - this.urls = new BlinkURLHandler(this._account_id, this._region_id); - } - - _getAuthToken(repeats = 0) { + setupSystem(name_or_id) { return new Promise((resolve, reject) => { - if (typeof this._username != 'string') { - return reject(new BlinkAuthenticationException("Username must be a string")); - } - if (typeof this._password != 'string') { - return reject(new BlinkAuthenticationException("Password must be a string")); - } - if (typeof this._device_id != 'string') { - return reject(new BlinkAuthenticationException("Device ID must be a string")); + + this.auth.startup().then(() => { + this.initialize(name_or_id).then(() => { + resolve(); + }); + }).catch((err) => { + if (err instanceof BlinkTwoFARequiredError) { + return this.auth.prompt2fa().then(() => { + this.initialize(name_or_id).then(() => { + resolve(); + }); + }); } - - let headers = { - 'Content-Type': 'application/json' - }; - const notification_key = guid(SIZE_NOTIFICATION_KEY); - let data = { - "email": this._username, - "password": this._password, - "notification_key": notification_key, - "unique_id": this._device_id, - "app_version": "6.0.12", - "client_name": this.device_name, - "client_type": "android", - "device_identifier": this._device_id, - "device_name": this.device_name, - "os_version": "5.1.1", - "reauth": "true", - }; - - let authenticate = (response) => { - // console.log('auth response: ', response); - - this._host = 'rest-' + this._region_id + '.' + BLINK_URL; - - this._auth_header = { - 'token-auth': this._token - }; - - this.urls = new BlinkURLHandler(this._account_id, this._region_id); - - resolve(true); - }; - - request.post({ - url: this.auth_2FA ? LOGIN_URL_2FA : LOGIN_URL, - json: true, - headers: headers, - body: data - }, (err, response, body) => { - if (err || _statusCodeIsError(response)) { - return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); - } else { - if ((body.account || {}).account_verification_required) { - if (!this.auth_2FA) { - if (repeats === 1) return reject(new BlinkAuthenticationException(`Authentication problem: verification timeout`)); - return new Promise((resolve, reject) => { - setTimeout(() => { - this._getAuthToken(repeats + 1).then(resolve, reject); - }, this.verification_timeout); - }).then(resolve, reject); - } - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - return rl.question(`Enter the verification code sent to ${this._username}: `, pin => { - this._region_id = body.account.tier; - this._region = body.account.region; - this._token = body.auth.token; - this._account_id = body.account.account_id; - - console.log('body.account.account_id', body.account.account_id); - const urls = new BlinkURLHandler(body.account.account_id, body.account.tier); - headers['token-auth'] = body.auth.token; - - // console.log(`url: ${urls.base_url}/api/v4/account/${body.account.account_id}/client/${body.account.client_id}/pin/verify`); - // console.log('headers: ' + JSON.stringify(headers)); - // console.log('pin: ' + pin); - - request.post({ - url: `${urls.base_url}/api/v4/account/${body.account.account_id}/client/${body.acount.client_id}/pin/verify`, - json: true, - headers: headers, - body: { - pin: `${pin}`, - } - }, (err, response, body) => { - // console.log('response: ' + JSON.stringify(body)); - - if (err || _statusCodeIsError(response)) { - return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); - } - if (!valid) { - return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); - } - - authenticate(body); - }); - rl.close(); - }); - } - if (!body.account.region) { - console.log('body ', body); - return reject(new BlinkAuthenticationException(body.message)); - } - - // console.log('body: ' + JSON.stringify(body)); - - this._region_id = body.account.tier; - this._region = body.account.region; - this._token = body.auth.token; - this._account_id = body.account.account_id; - - authenticate(body); - } - }); + reject(err); }); - } + }); +} getIDs(name_or_id) { var that = this; return new Promise((resolve, reject) => { - if (!this._auth_header) { + if (!this.auth.hasToken()) { return reject(new BlinkException("You have to be authenticated before calling this method")); } - request({ - url: this.urls.home_url, - headers: this._auth_header, - json: true - }, (err, response, body) => { - if (err || _statusCodeIsError(response)) { - console.log('error ', body); + + api.getIDs(this).then((response) => { + const body = response.body; + if (_statusCodeIsError(response.status)) { + Logger.error('error ', body); return reject(new BlinkException(`Can't retrieve system status`)); } else { var network = false; @@ -457,10 +332,7 @@ module.exports = class Blink { }); } - // console.log('account: ' + JSON.stringify(body.account)); - that._account_id = body.account.id; - that.urls = new BlinkURLHandler(that._account_id, that.regionId); return resolve(that); } }); diff --git a/lib/blink_2fa_exception.js b/lib/blink_2fa_exception.js new file mode 100644 index 0000000..f49dddf --- /dev/null +++ b/lib/blink_2fa_exception.js @@ -0,0 +1,7 @@ +module.exports = class BlinkTwoFARequiredError extends Error { + constructor(message) { + super(message); + this.name = "BlinkTwoFARequiredError"; + Object.setPrototypeOf(this, BlinkTwoFARequiredError.prototype); // Important for instanceof checks + } +} diff --git a/lib/blink_auth.js b/lib/blink_auth.js new file mode 100644 index 0000000..0f6f18b --- /dev/null +++ b/lib/blink_auth.js @@ -0,0 +1,331 @@ +const request = require('request'); +const api = require('./api'); +const util = require('./util'); +const Logger = require('./logger'); +const BlinkTwoFARequiredError = require('./blink_2fa_exception'); + +/** + * Centralize http operations and handle authentication + */ + +module.exports = class BlinkAuth { + constructor(loginData, noPrompt=false, callback=null) { + this.tierInfo = null; + this.regionId = null; + this.noPrompt = noPrompt; + this.callback = callback; + + if (loginData) { + this.username= loginData.username || null; + this.password = loginData.password || null; + this.deviceId = loginData.deviceId || null; + this.token = loginData.token || null; + this.expiresIn = loginData.expiresIn || null; + this.expirationDate = loginData.expirationDate || null; + this.refreshToken = loginData.refreshToken || null; + this.regionId = loginData.regionId || null; + this.host = loginData.host || null; + this.accountId = loginData.accountId || null; + } + } + + hasToken() { + return !!this.token; + } + + getHeaders() { + if (!this.hasToken()) { + return null; + } + + return { + Authorization: `Bearer ${this.token}`, + "Content-Type": "application/json", + }; + } + + // use this to store the credentials for a later login + getLoginAttributes() { + return { + token : this.token, + expiresIn : this.expiresIn, + expirationDate : this.expirationDate, + refreshToken : this.refreshToken, + host : this.host, + regionId : this.regionId, + accountId : this.accountId, + } + } + + getLoginData() { + return { + username: this.username, + password: this.password, + deviceId: this.deviceId, + twoFACode : this.twoFACode, + } + } + + validateLogin() { + this.username = this.username || null; + this.password = this.password || null; + + if (!this.noPrompt) { + const loginData = util.promptLoginData(this.username, this.password); + this.username = loginData.username; + this.password = loginData.password; + } + + const loginData = util.validateLoginData(this.uid, this.deviceId); + this.uid = loginData.uid; + this.deviceId = loginData.deviceId; + } + + hasLoginAttributes() { + const values = Object.values(this.getLoginAttributes()); + for (const value of values) { + if (!value) return false; + } + return true; + } + + login(loginUrl = LOGIN_ENDPOINT, refresh = false) { + return new Promise((resolve, reject) => { + this.validateLogin(); + + api.requestLogin( + this, + loginUrl, + this.getLoginData(), + refresh + ).then((response) => { + if (response && response.status === 200) { + resolve(JSON.parse(response.body)); + return; + } + + if (response && response.status === 401) { + Logger.error("Invalid refresh token or invalid credentials."); + Logger.debug(response); + } + + if (response && response.status === 412) { + reject(new BlinkTwoFARequiredError("2FA required")); + return; + } + + reject(new Error("Login error " + response.status )); + }); + }); + } + + getTierInfo(tierUrl = TIER_ENDPOINT) { + return new Promise((resolve, reject) => { + api.requestTier(this, tierUrl).then((response) => { + if (response && response.status === 200) { + resolve(JSON.parse(response.body)); + return; + } + + reject(new Error("Tier info error " + response.status )); + }); + }); + } + + refreshTokens(refresh = false) { + return new Promise((resolve, reject) => { + this.isErrored = true; + + try { + Logger.info( + `${refresh ? "Refreshing" : "Obtaining"} authentication token.` + ); + + this.login(LOGIN_ENDPOINT, refresh).then((loginResponse) => { + this.setLoginInfo( loginResponse ); + + if (!refresh) { + this.getTierInfo().then((tierInfo) => { + this.setTierInfo(tierInfo); + this.isErrored = false; + resolve(); + }); + } else { + this.isErrored = false; + resolve(); + } + + }).catch((err) => { + reject(err); + }); + + } catch (e) { + if (e instanceof BlinkTwoFARequiredError) { + throw e; + } else { + throw e; + } + } + }); + } + + setLoginInfo(loginResponse) { + this.token = loginResponse.access_token; + this.expiresIn = loginResponse.expires_in; + this.expirationDate = new Date().getTime() / 1000 + (this.expiresIn || 0); + this.refreshToken = loginResponse.refresh_token; + } + + setTierInfo(tierInfo) { + if (!tierInfo) { + throw new Error("No tier info to extract region and account from"); + } + + this.regionId = tierInfo["tier"]; + this.host = `${this.regionId}.${BLINK_URL}`; + this.accountId = tierInfo["account_id"]; + } + + prompt2fa() { + const code = util.prompt2faCode(); + if (!code) { + throw new Error("2FA code is required"); + } + return this.send2faCode(code); + } + + send2faCode(code) { + this.twoFACode = code; + return this.startup(); + } + + startup() { + return new Promise((resolve, reject) => { + this.validateLogin(); + + if (!this.hasLoginAttributes()) { + this.refreshTokens().then(() => { + resolve(); + }).catch((err) => { + reject(err); + }); + return; + } else { + Logger.debug('has login attributes'); + } + + resolve(); + }); + } + + refreshCheck(headers) { + return new Promise( (resolve, reject) => { + Logger.debug('refreshing tokens'); + this.refreshTokens(true).then(() => { + if ("Authorization" in headers) { + // update the authorization header with the new token + headers["Authorization"] = `Bearer ${this.token}`; + } + + if (this.callback !== null) { + this.callback(this.getLoginAttributes()); + + resolve(headers); + } + }); + }); + } + + needRefresh(skipRefreshCheck) { + if (skipRefreshCheck) return false; + + if (!this.expirationDate) { + return !!this.refreshToken; + } + + return this.expirationDate - new Date().getTime() / 1000 < 60; + } + + get(url, headers, json=false, skipRefreshCheck=false, binary=false) { + return new Promise((resolve, reject) => { + Logger.debug('get: ' + url); + + if (this.needRefresh(skipRefreshCheck)) { + this.refreshCheck(headers).then( (updatedHeaders) => { + request({ + url: url, + json: json, + headers: updatedHeaders, + encoding: binary ? null : undefined + }, (err, response) => { + if (err) { + Logger.error( err.message); + reject(err); + } else { + resolve({status: response.statusCode, body: response.body}); + } + }) + + }); + } else { + request({ + url: url, + json: json, + headers: headers, + encoding: binary ? null : undefined + }, (err, response) => { + if (err) { + Logger.error( err.message); + reject(err); + } else { + resolve({status: response.statusCode, body: response.body}); + } + }) + } + }); + } + + post(url, headers, body, json=false, skipRefreshCheck=false) { + return new Promise((resolve, reject) => { + Logger.debug('post: ' + url); + + if (this.needRefresh(skipRefreshCheck)) { + this.refreshCheck(headers).then( (updatedHeaders) => { + request.post({ + url: url, + json: json, + headers: updatedHeaders, + body: body + }, (err, response, body) => { + if (err) { + Logger.error( err.message); + reject(err); + } else { + resolve({status: response.statusCode, body: json ? JSON.parse(body) : body}); + } + }) + }); + } else { + request.post({ + url: url, + json: json, + headers: headers, + body: body + }, (err, response, body) => { + if (err) { + Logger.error( err.message); + reject(err); + } else { + try { + const parsedBody = json && body instanceof String ? JSON.parse(body) : body; + resolve({status: response.statusCode, body: parsedBody}); + } catch (e) { + Logger.debug('body: ', JSON.stringify(body)) + reject(e); + } + } + }) + } + }); + } +}; \ No newline at end of file diff --git a/lib/blink_camera.js b/lib/blink_camera.js index 7d4176d..4292341 100644 --- a/lib/blink_camera.js +++ b/lib/blink_camera.js @@ -2,11 +2,11 @@ * Created by madshall on 3/17/17. */ const BlinkException = require('./blink_exception'); - -const request = require('request'); +const api = require('./api'); module.exports = class BlinkCamera { - constructor(config, urls) { + constructor(blink, config, urls) { + this.blink = blink; this.urls = urls; this._id = config.id; this._name = config.name; @@ -137,35 +137,21 @@ module.exports = class BlinkCamera { snapPicture() { return new Promise((resolve, reject) => { - request.post({ - url: this._image_link, - json: true, - headers: this._header, - body: {} - }, (err, response, body) => { - if (err) { - reject(new BlinkException(`Can't get snapshot from camera ${this._id}:${this._name}`)); - } else { - resolve(body); - } - }) + api.snapPicture(this).then((response) => { + resolve(response.body); + }).catch(() => { + reject(new BlinkException(`Can't snap picture for camera ${this._id}:${this._name}`)); + }); }); } setMotionDetect(enable) { return new Promise((resolve, reject) => { - request.post({ - url: this._arm_link + (enable ? 'enable' : 'disable'), - json: true, - headers: this._header, - body: {} - }, (err, response, body) => { - if (err) { - reject(new BlinkException(`Can't set motion detect for camera ${this._id}:${this._name}`)); - } else { - resolve(body); - } - }) + api.setMotionDetect(this, enable).then((response) => { + resolve(response.body); + }).catch(() => { + reject(new BlinkException(`Can't set motion detect for camera ${this._id}:${this._name}`)); + }); }); } @@ -182,43 +168,29 @@ module.exports = class BlinkCamera { imageRefresh() { return new Promise((resolve, reject) => { - request({ - url: this.urls.home_url, - headers: this._header, - json: true - }, (err, response, body) => { - if (err) { - reject(new BlinkException(`Can't refresh thumbnail for camera ${this._id}:${this._name}`)); - } else { - let cameras = body.cameras; - cameras.forEach((camera) => { - if (camera.id === this._id) { - this._thumb = this.urls.base_url + camera['thumbnail'] + '.jpg'; - this._updated_at = camera['updated_at']; - resolve(this._thumb); - } - }); - resolve(null); - } - }) + api.imageRefresh(this.blink).then((response) => { + let cameras = body.cameras; + cameras.forEach((camera) => { + if (camera.id === this._id) { + this._thumb = this.urls.base_url + camera['thumbnail'] + '.jpg'; + this._updated_at = camera['updated_at']; + resolve(this._thumb); + } + }); + resolve(null); + }).catch(() => { + reject(new BlinkException(`Can't refresh thumbnail for camera ${this._id}:${this._name}`)); + }); }); } statusRefresh() { return new Promise((resolve, reject) => { - request.post({ - url: this._arm_link + 'status', - json: true, - headers: this._header, - body: {} - }, (err, response, body) => { - if (err) { + api.statusRefresh(this).then((response) => { + resolve(response.body); + }).catch(() => { reject(new BlinkException(`Can't refresh status for camera ${this._id}:${this._name}`)); - } else { - let status = body; - resolve(status); - } - }) + }); }); } @@ -227,50 +199,31 @@ module.exports = class BlinkCamera { // console.log('thumbnail ' + this.thumbnail); // console.log('header ' + this._header); - request({ - url: this.thumbnail, - headers: this._header, - encoding: null - }, (err, response, body) => { - if (err) { + api.fetchImageData(this).then((response) => { + resolve(response.body); + }).catch(() => { reject(new BlinkException(`Can't refresh thumbnail for camera ${this._id}:${this._name}`)); - } else { - resolve(body); - } - }) + }); }); } fetchVideoData() { return new Promise((resolve, reject) => { - request({ - url: this.clip, - headers: this._header, - encoding: null - }, (err, response, body) => { - if (err) { + api.fetchVideoData(this).then((response) => { + resolve(response.body); + }).catch(() => { reject(new BlinkException(`Can't refresh thumbnail for camera ${this._id}:${this._name}`)); - } else { - resolve(body); - } - }) + }); }); } recordClip() { return new Promise((resolve, reject) => { - request.post({ - url: this._arm_link + 'clip', - json: true, - headers: this._header, - body: {} - }, (err, response, body) => { - if (err) { + api.recordClip(this).then((response) => { + resolve(response.body); + }).catch(() => { reject(new BlinkException(`Can't record clip for camera ${this._id}:${this._name}`)); - } else { - resolve(body); - } - }) + }); }); } }; diff --git a/lib/constants.js b/lib/constants.js index 92e9d89..24a3213 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -9,3 +9,22 @@ global.BASE_URL = 'https://rest-prod.' + BLINK_URL; global.DEFAULT_URL = 'prod.' + BLINK_URL; global.SIZE_NOTIFICATION_KEY = 152; global.SIZE_UID = 16; + +global.DEFAULT_USER_AGENT = "27.0ANDROID_28373244"; +global.APP_BUILD = "ANDROID_28373244"; +global.DEVICE_ID = "node-blink-security"; +global.DEFAULT_MOTION_INTERVAL = 1; +global.DEFAULT_REFRESH = 30; +global.TIMEOUT = 10; + +global.OAUTH_BASE_URL = "https://api.oauth.blink.com"; +global.LOGIN_ENDPOINT = `${OAUTH_BASE_URL}/oauth/token`; +global.TIER_ENDPOINT = `${BASE_URL}/api/v1/users/tier_info`; + +// OAuth +global.OAUTH_GRANT_TYPE_PASSWORD = "password"; +global.OAUTH_GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; +global.OAUTH_SCOPE = "client"; +global.OAUTH_CLIENT_ID = "android"; + +global.DEBUG = false; \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..4cb300f --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,17 @@ +const Logger = { + info: (message) => { + console.info(message); + }, + + error: (message) => { + console.error(message); + }, + + debug: (message) => { + if (DEBUG) { + console.debug(message); + } + }, +}; + +module.exports = Logger; diff --git a/lib/util.js b/lib/util.js index 45af97f..1596415 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,15 +1,54 @@ -const guid = (len = 32) => { - let buf = [], - chars = 'abcdef0123456789', - charlen = chars.length; +const prompt = require("prompt-sync")(); - for (var i = 0; i < len; i++) { - buf[i] = chars.charAt(Math.floor(Math.random() * charlen)); +function tokenHex(nbytes = 32) { + // Create a Uint8Array to store the random bytes. + // The size of the array determines the number of random bytes generated. + const randomBytes = new Uint8Array(nbytes); + + // Fill the array with cryptographically secure random values. + crypto.getRandomValues(randomBytes); + + // Convert the Uint8Array to a hexadecimal string. + // Each byte (0-255) is converted to a two-digit hexadecimal representation. + return Array.from(randomBytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +} + +const promptLoginData = (username, password) => { + if (!username) { + username = prompt("Username: "); + } + if (!password) { + password = prompt("Password: ", { echo: "*" }); } + return {username, password}; +}; - return buf.join(''); +const prompt2faCode = () => { + const code = prompt("Enter the two-factor authentication code: "); + return code; } +const genUid = (size, uidFormat = false) => { + if (uidFormat) { + const token = `BlinkCamera_${tokenHex(4)}-${tokenHex(2)}-${tokenHex( + 2 + )}-${tokenHex(2)}-${tokenHex(6)}`; + return token; + } + + const token = tokenHex(size); + return token; +}; + +const validateLoginData = (uid, deviceId) => { + return {uid: uid || genUid(SIZE_UID, true), + deviceId: deviceId || DEVICE_ID}; +}; + module.exports = { - guid, -}; \ No newline at end of file + promptLoginData, + prompt2faCode, + validateLoginData +}; diff --git a/package.json b/package.json index 4c64791..10edaa3 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,7 @@ "system" ], "author": "Dmitry Ivanov ", - "license": { - "type": "MIT", - "url": "https://github.com/madshall/node-blink-security/blob/master/LICENSE" - }, + "license": "MIT", "bugs": { "url": "https://github.com/madshall/node-blink-security/issues" }, @@ -29,6 +26,7 @@ "node": ">=6.0.0" }, "dependencies": { + "prompt-sync": "^4.2.0", "request": "^2.88.0" }, "devDependencies": {