"use strict";
/** @module */
module.exports = WirelessTagPlatform;
var request = require('request'),
http = require('http'),
util = require('util'),
delay = require('timeout-as-promise'),
EventEmitter = require('events');
var u = require('./util'),
WirelessTagManager = require('./tagmanager');
/**
* @const {string} - The default base URI for the JSON API server.
* @default
*/
const API_BASE_URI = 'https://www.mytaglist.com';
/**
* @const {number} - The time in milliseconds to wait before retrying an
* operation that failed because the tag did not respond.
* @default
*/
const WAIT_BEFORE_RETRY = 8000;
/**
* Instantiates {@link WirelessTagPlatform}.
*
* @param {Object} [options]
* @param {String} [options.log] - a custom log function.
* @param {String} [options.errorHandler] - a function returning a custom
* error handler, will be passed a callback function.
* @param {String} [options.apiBaseURI] - the base URI of the API
* server if hosted on a different server than
* the default ([API_BASE_URI]{@link
* module:lib/platform~API_BASE_URI})
*
* @class
* @alias WirelessTagPlatform
*/
function WirelessTagPlatform(config) {
EventEmitter.call(this);
if (config === undefined) config = {};
this.log = config.log || console.log;
this.errorHandler = config.errorHandler || u.defaultHandler;
this.apiBaseURI = config.apiBaseURI || API_BASE_URI;
this.callAPI = WirelessTagPlatform.callAPI;
}
util.inherits(WirelessTagPlatform, EventEmitter);
/**
* Connect event. Emitted after the platform object successfully
* connects to the cloud.
*
* @event WirelessTagPlatform#connect
* @type {WirelessTagPlatform}
*/
/**
* Discover event. Emitted for every {@link WirelessTagManager}
* instance discovered.
*
* @event WirelessTagPlatform#discover
* @type {WirelessTagManager}
*/
/**
* Connects to the cloud API if not connected already. Note that the
* {@link WirelessTagPlatform#event:connect} event will not fire if
* already connected.
*
* @param {Object} opts - connection parameters
* @param {String} opts.username - the username (email) for connecting
* @param {String} opts.password - the password for connecting
* @param {module:wirelesstags~apiCallback} [callback]
*
* @fires WirelessTagPlatform#connect
* @returns {Promise} resolves to 'this' upon success
*/
WirelessTagPlatform.prototype.connect = function(opts, callback) {
return this.isConnected().then(
(connected) => {
if (connected) return this;
return this.callAPI(
'/ethAccount.asmx/Signin',
{ email: opts.username, password: opts.password },
callback)
.then(
(res) => {
this.emit('connect', this);
if (callback) callback(null, { object: this });
return this;
},
this.errorHandler(callback)
);
},
this.errorHandler(callback)
);
};
/**
* Tests whether this instance is connected to the cloud API.
*
* @param {module:wirelesstags~apiCallback} [callback]
*
* @returns {Promise} resolves to true if connected, and false otherwise
*/
WirelessTagPlatform.prototype.isConnected = function(callback) {
return this.callAPI('/ethAccount.asmx/IsSignedIn', {}, callback).
then((res) => {
if (callback) callback(null, { object: this, value: res });
return res;
});
};
/**
* Retrieves the tag managers available to the connected account. The
* list is optionally filtered depending on the supplied query
* parameter.
*
* Note that using the 'query' parameter as opposed to filtering the
* returned tag manager objects will really only be useful to prevent
* 'discover' events from being fired for undesired tag manager
* objects. The filtering does not happen at the API endpoint, and so
* has almost no performance benefits, unless there are many tag
* managers under the account and the 'discover' event listener were
* somehow expensive to execute.
*
* @param {Object} [query] - an object with keys and values that a tag
* manager data object returned by the API has to
* meet. The most useful ones are likely 'name' and
* 'mac'. Consult the [GetTagManagers JSON API]{@link
* http://wirelesstag.net/media/mytaglist.com/ethAccount.asmx@op=GetTagManagers.html}
* for possible keys.
* @param {module:wirelesstags~apiCallback} [callback]
*
* @fires WirelessTagPlatform#discover
* @returns {Promise} resolves to an array of (optionally filtered)
* {@link WirelessTagManager} instances
*/
WirelessTagPlatform.prototype.discoverTagManagers = function(query, callback) {
var req = this.callAPI(
'/ethAccount.asmx/GetTagManagers',
{},
callback);
return req.then(
(result) => {
let tagManagers = [];
result = result.filter(u.createFilter(query));
for (let mgrData of result) {
let tagManager = new WirelessTagManager(this, mgrData);
tagManagers.push(tagManager);
this.emit('discover', tagManager);
}
if (callback) callback(null, { object: this, value: tagManagers });
return tagManagers;
},
this.errorHandler(callback)
);
};
/**
* Selects the given tag manager for subsequent API calls that expect
* it, if the tag manager is not already selected. Note that the
* library will call this automatically, and so a user will not
* normally need to do so.
*
* @param {WirelessTagManager} tagManager - the tag manager instance to select
* @param {module:wirelesstags~apiCallback} [callback]
*
* @returns {Promise} resolves to the tag manager instance
*/
WirelessTagPlatform.prototype.selectTagManager = function(tagManager,callback) {
if (tagManager.selected) return Promise.resolve(tagManager);
var req = this.callAPI(
'/ethAccount.asmx/SelectTagManager',
{ mac: tagManager.mac },
callback);
return req.then(
(result) => {
tagManager.data.selected = true;
if (callback) callback(null, { object: this, value: tagManager });
return tagManager;
},
this.errorHandler(callback)
);
};
/**
* Queries and/or sets whether failed API calls should be retried. By
* default this is off.
*
* Note that failed calls are only retried under certain conditions,
* and only for a certain number of times. At present, the condition
* is that failure be due to the tag not responding, and the call is
* retried only once, after waiting [WAIT_BEFORE_RETRY]{@link
* module:lib/platform~WAIT_BEFORE_RETRY} milliseconds.
*
* @param {boolean} [enable] - on set, whether or not to enable retrying
* @returns {boolean} whether or not retrying is currently enabled
*/
WirelessTagPlatform.prototype.retryOnError = function(enable) {
if (enable !== undefined) this._retryAPICalls = enable;
return this._retryAPICalls || false;
};
/**
* Performs a call to the cloud JSON API. Users should not normally
* need to call this method directly.
*
* Note that the method tries to infer necessary pre-steps based on
* the value of 'this'. It is thus meant to be called as an instance
* method, with this set to the instance in the library's class
* hierarchy from where the call would be coming. For example,
* tag-specific calls should have 'this' bound to a {@link
* WirelessTag} instance.
*
* @param {string} uri - the uri for the API endpoint to be called;
* will be prefixed with the base URI if not an
* absolute URI.
* @param {object} reqBody - the request body as an object
* @param {module:wirelesstags~apiCallback} [callback] - note that
* this method will only call this in the event of
* error, and it is the caller's responsibility to
* call it with the appropriately processed return
* value in case of success.
*
* @returns {Promise} Resolves to the value of the 'd' property of the
* response body from the API endpoint (or the body
* itself if there is no 'd' property). Invokes
* error handler function on error. The default
* handler will rethrow the error, resulting in
* rejecting the promise.
*/
WirelessTagPlatform.callAPI = function(uri, reqBody, callback) {
let platform, tagManager;
if (this instanceof WirelessTagPlatform) {
platform = this;
// in this case no tag manager, and none should be needed
} else if (this instanceof WirelessTagManager) {
tagManager = this;
} else {
// caller could be sensor or tag instance, try first as sensor
let tag = this.wirelessTag;
// if not sensor, try as tag
tagManager = tag ? tag.wirelessTagManager : this.wirelessTagManager;
}
if (tagManager) platform = tagManager.wirelessTagPlatform;
// prefix with base URI if not already an absolute URI
if (! (uri.startsWith('https://') || uri.startsWith('http://'))) {
let api_base = platform ? platform.apiBaseURI : API_BASE_URI;
uri = api_base + uri;
}
// if we got a tag manager instance, ensure it's selected, as this is
// unfortunately required by most API methods
let selectTask = tagManager ? tagManager.select() : Promise.resolve();
let apiCall = selectTask.then(() => {
return makeAPICall(uri, reqBody);
}).catch((e) => {
if (platform && platform.retryOnError() &&
(e instanceof TagDidNotRespondError)) {
// we retry this only once
return delay(WAIT_BEFORE_RETRY).then(() => {
return makeAPICall(uri, reqBody);
});
}
let handler = platform ?
platform.errorHandler(callback) : u.defaultHandler(callback);
handler(e);
});
return apiCall;
};
function makeAPICall(uri, reqBody) {
let apiCall = new Promise((resolve, reject) => {
request({
method: 'POST',
uri: uri,
json: true,
jar: true,
gzip: true,
body: reqBody || {}
}, function (error, response, body) {
error = checkAPIerror(error, response, uri, reqBody, body);
if (error) return reject(error);
resolve(body.d === undefined ? body : body.d);
});
});
return apiCall;
}
var APICallError = require('./error/APICallError'),
TagDidNotRespondError = require('./error/TagDidNotRespondError');
WirelessTagPlatform.APICallError = APICallError;
WirelessTagPlatform.TagDidNotRespondError = TagDidNotRespondError;
WirelessTagPlatform.DuplicateEthCmdError = require('./error/DuplicateEthCmdError');
WirelessTagPlatform.OperationIncompleteError = require('./error/OperationIncompleteError');
WirelessTagPlatform.RetryUnsuccessfulError = require('./error/RetryUnsuccessfulError');
WirelessTagPlatform.TagManagerOfflineError = require('./error/TagManagerOfflineError');
WirelessTagPlatform.UnauthorizedAccessError = require('./error/UnauthorizedAccessError');
WirelessTagPlatform.OperationUnsupportedError = require('./error/OperationUnsupportedError');
function checkAPIerror(error, response, uri, reqBody, body) {
if (error) return error;
if (! response) return new Error("undefined response for URI " + uri);
if (response.statusCode != 200) {
let apiCallProps = { statusCode: response.statusCode,
requestBody: reqBody,
url: uri };
let APIError = APICallError;
let message = http.STATUS_CODES[response.statusCode];
if (body) {
if (body.ExceptionType) {
let typeMatch = body.ExceptionType.match(/^\w+\.\w+\+(\w+)$/);
if (typeMatch === null) {
typeMatch = body.ExceptionType.match(/^\w+\.(\w+)$/);
}
if (typeMatch !== null) {
let errorType = typeMatch[1].replace("Exception","Error");
if (WirelessTagPlatform[errorType]) {
APIError = WirelessTagPlatform[errorType];
} else {
message = body.ExceptionType;
}
} else {
message = body.ExceptionType;
}
}
if (body.Message) message += ": " + body.Message;
}
return new APIError(message, apiCallProps);
}
return null;
}