location.js

'use strict';

const _request = require('request-promise');
const _ical = require('ical.js');

const _fs = require('fs');
const _url = require('url-parse');
const _express = require('express');
const _https = require('https');
const _http = require('http');

const _events = require('events').EventEmitter;
const _util = require('util');

const TimeWindow = require('./timewindow');


/**
 * The constructor for Nello locations.
 *
 * @class Location
 * @param {Nello}			connection		Nello instance
 * @param {String}			locationId		ID of the Nello location
 * @returns void
 * @constructor
 */
var Location = function Location(Nello, locationId)
{
	_events.call(this);
	
	this.parent = Nello;
	this.locationId = locationId;
};

/**
 * This function requests an action.
 *
 * @memberof Location
 * @param {String|Array}	params			Parameters to attach to URL
 * @param {String}			[method=GET]	Method to use [GET, POST, PUT, DELETE]
 * @param {String}			[body={}]		Body data to send
 * @param {String}			[options={}]	Additional options to use
 * @returns {Promise<Object>}
 * @private
 */
Location.prototype._req = function _req(params, method, body, options)
{
	params = 'locations/' + this.locationId + '/' + (params ? ('/' + (typeof params === 'string' ? params : params.join('/'))) : '');
	return _request(Object.assign(options || {},
	{
		url: 'https://public-api.nello.io/v1/' + params + '/',
		method: method || 'GET',
		headers: {'Authorization': 'Bearer ' + this.parent.token},
		body: body || {},
		json: true
	}));
};

_util.inherits(Location, _events);


/**
 * This function opens the door.
 *
 * @memberof Location
 * @param void
 * @returns {Promise<Boolean>}
 */
Location.prototype.openDoor = function openDoor()
{
	var self = this;
	return this
		._req('open', 'PUT')
		.then(function(res)
		{
			return !!res.result.success;
		});
};

/**
 * This function retrieves all time windows.
 *
 * @memberof Location
 * @param void
 * @returns {Promise<TimeWindowInfo[]>}
 */
Location.prototype.getTimeWindows = function getTimeWindows()
{
	var self = this;
	return this
		._req('tw')
		.then(function(res)
		{
			if (res.result === undefined || res.result.success !== true || !Array.isArray(res.data))
				throw new Error('Did not receive any time windows!');
			
			return res.data;
		})
		.map(function(tw)
		{
			// enrich data
			var ical = {_raw: JSON.stringify(tw.ical)};
			var vevent = new _ical.Component(_ical.parse(tw.ical)).getFirstSubcomponent('vevent');
			['UID', 'SUMMARY', 'DTSTAMP', 'DTSTART', 'DTEND'].forEach(function(key)
			{
				ical[key.toLowerCase()] = vevent.getFirstPropertyValue(key.toLowerCase()) || null;
			});
			
			ical.rrule = new _ical.Recur(vevent.getFirstPropertyValue('rrule'));
			tw.ical = ical;
			
			tw.action = new TimeWindow(self.parent, self.locationId, tw.id);
			return tw;
		});
};

/**
 * This function retrieves a single time window.
 *
 * @memberof Location
 * @param {String}			twId			ID of the time window
 * @returns {Promise<TimeWindowInfo>}
 */
Location.prototype.getTimeWindow = function getTimeWindow(twId)
{
	var self = this;
	return this.getTimeWindows()
		.then(function(tws)
		{
			var found = null;
			tws.map(function(tw)
			{
				if (tw.id == twId)
					found = tw;
			});
			
			if (found === null)
				throw new Error('Did not find the requested Nello time window!');
			else
				return found;
		});
};

/**
 * This function adds a new time window.
 *
 * @memberof Location
 * @param {Object}			data			Data of the new time window
 * @param {String}			data.name		Name of the new time window
 * @param {String}			data.ical		ICal data of the new time window
 * @returns {Promise<Object>}
 */
Location.prototype.addTimeWindow = function addTimeWindow(data)
{
	var self = this;
	return this
		._req('tw', 'POST', data)
		.then(function(res)
		{
			if (res.result.success !== true)
				throw new Error(res.result.message);
			else
				return res.data;
		});
};

/**
 * This function removes a specfic time window.
 *
 * @memberof Location
 * @param {String}			twId			ID of the time window
 * @returns {Promise<Boolean>}
 */
Location.prototype.removeTimeWindow = function removeTimeWindow(twId)
{
	var self = this;
	return this.getTimeWindows()
		.then(function(tws)
		{
			var removed = false;
			tws.map(function(tw)
			{
				if (tw.id === twId)
					removed = tw.action.remove();
			});
			
			return removed;
		});
};

/**
 * This function removes all time windows.
 *
 * @memberof Location
 * @param void
 * @returns {Promise<Boolean>}
 */
Location.prototype.removeAllTimeWindows = function removeAllTimeWindows()
{
	var self = this;
	return this.getTimeWindows()
		.then(function(tws)
		{
			var removed = [];
			tws.map(function(tw)
			{
				removed.push(tw.action.remove());
			});
			
			return Promise.all(removed);
		});
};

/**
 * This function adds / attaches a webhook.
 *
 * @memberof Location
 * @param {String}			url													URL of the webhook
 * @param {Object}			[ssl]												SSL configuration data
 * @param {String}			ssl.key												SSL Private key (either string to file or key itself)
 * @param {String}			ssl.cert											SSL Certificate (either string to file or key itself)
 * @param {String}			ssl.ca												SSL Certificate authority (either string to file or key itself)
 * @param {Array}			[actions=['swipe', 'geo', 'tw', 'deny']]			Allowed nello actions to listen to
 * @param {Boolean}			[listen=false]										State whether listener shall be attached to webhook
 * @param {Array}			[methods=['GET', 'POST', 'PUT', 'DELETE']]			Allowed HTTP methods to the webhook
 * @returns {Promise<Object>}
 */
Location.prototype.addWebhook = function addWebhook(url, ssl, actions, listen, methods)
{
	var self = this;
	url = url.indexOf('http') === -1 ? ((ssl !== undefined && ssl !== false ? 'https://' : 'http://') + url) : url;
	
	return this
		._req('webhook', 'PUT', {'url': url, 'actions': actions || ['swipe', 'geo', 'tw', 'deny']})
		.then(function(res)
		{
			// attach listener
			if (listen === true)
				self.addListener(url, ssl, methods);
			
			return res.result.success === true ? url : false;
		});
};

/**
 * This function adds a listener on an URL respectively a webhook to receive events.
 *
 * @memberof Location
 * @param {String}			url													URL of the webhook
 * @param {Array}			[methods=['GET', 'POST', 'PUT', 'DELETE']]			Allowed HTTP methods to the webhook
 * @param {Object}			[ssl]												SSL configuration data
 * @param {String}			ssl.key												SSL Private key (either string to file or key itself)
 * @param {String}			ssl.cert											SSL Certificate (either string to file or key itself)
 * @param {String}			ssl.ca												SSL Certificate authority (either string to file or key itself)
 * @returns void
 */
Location.prototype.addListener = function addListener(url, methods, ssl)
{
	var self = this;
	url = url.indexOf('http') === -1 ? ((ssl !== undefined && ssl !== false ? 'https://' : 'http://') + url) : url;
	methods = methods === undefined ? ['GET', 'POST', 'PUT', 'DELETE'] : methods;
	
	// get port and path from URL
	var parsedUrl = _url(url, true);
	var port = parsedUrl.port || 80;
	var path = parsedUrl.pathname;
	
	// start server
	var server;
	var app = _express();
	app.use(_express.json());
	
	// validate SSL
	if (ssl !== undefined && (ssl.cert === undefined || ssl.key === undefined))
		throw new Error('Incomplete SSL configuration! Please provide a certificate and a private key.');
	
	// attach to https server
	else if (ssl !== undefined && ssl.cert !== undefined && ssl.key !== undefined)
		server = _https.createServer({
			key: ssl.key.indexOf('.') === -1 ? ssl.key : _fs.readFileSync(ssl.key),
			cert: ssl.cert.indexOf('.') === -1 ? ssl.cert : _fs.readFileSync(ssl.cert),
			ca: ssl.ca.indexOf('.') === -1 ? ssl.ca : _fs.readFileSync(ssl.ca),
			//rejectUnauthorized: false,
		}, app);
	
	// attach to http server
	else
		server = _http.createServer(app);
	
	// handle requests
	app.all(path, function(req, res)
	{
		if (methods.indexOf(req.method) === -1)
			res.sendStatus(403);
		
		var body = req.body;
		if (body)
		{
			self.emit('webhook', body);
			self.emit(body.action, body.data);
			self.parent.emit('webhook', body);
			self.parent.emit(body.action, body.data);
		}
	});
	
	// listen for events
	this.listener = server.listen(port);
};

/**
 * This function removes a webhook.
 *
 * @memberof Location
 * @param void
 * @returns {Promise<Boolean>}
 */
Location.prototype.removeWebhook = function removeWebhook()
{
	var self = this;
	return this
		._req('webhook', 'DELETE')
		.then(function(res)
		{
			if (self.listener)
				self.listener.close();
			
			return !!res.result.success;
		});
};


module.exports = Location;


/**
 * @typedef {Object}		TimeWindowInfo
 * @property {Number}		id						ID of the time window
 * @property {String}		name					Name of the time window
 * @property {String}		image					Path to image of the time window (not used by nello API)
 * @property {Boolean}		enabled					Indication whether time window is enabled
 * @property {Boolean}		state					State of the time window
 * @property {Object}		ical					Ical data of the time window
 * @property {TimeWindow}	action					TimeWindow instance
 */