Source: api.js

/**
 * API module.
 * @module API
 */
const fs = require( 'fs' );
const qs = require( 'qs' );
const core = require( './src/core' );
const users = require( './src/users.js' );
const uniqid = require( 'uniqid' );
const request = require( 'request' );
const express = require( 'express' );
const router = express.Router( );

const detectorHandler = new core.DetectorHandler( );

/**
 * <strong>ENDPOINT.</strong><br/>
 * The root (<tt>/</tt>) endpoint allows us to create a <strong>user session</strong>.<br/>
 * Tot groups each set of detectors (and hence results) under an user object
 * (see [Users]{@link module:Users}), univocally identified with an id (generated with
 * <a target="_blank" href="https://www.npmjs.com/package/uniqid">uniqid</a>) stored as a
 * <strong>cookie</strong>, so the users have to send their id along with their request in
 *  order to perform any operation. <strong>The users cannot communicate with any other
 * endpoint if they haven't been given an id</strong>.
 * When a new request for this endpoint arrives, the request's cookies are checked:
 * <ul>
 * 	<li>If there is no id, then a new user is created, and its unique id is stored in
 * the response's cookies.</li>
 * 	<li>If there is an user id in the cookies, then that user's session is refreshed.
 * If said id is from an expired user, then a new user (and id) is created.
 *
 * @function /
 */
router.get( '/', function( req, res, next ) {
	if ( req.cookies && users.userExists( req.cookies.userId ) ) {
		console.log( 'Existing user. Session refreshed' );
	} else {
		const userId = users.addUser( uniqid( ) );
		console.log( 'User added' );
		res.cookie( 'userId', userId );
	}
	res.send( 'respond with a resource' );
} );

/**
 * This middleware is set at the beginning of the API, after the <code>/</code> endpoint,
 * to check if the user has a valid id.<br/>
 * If there is no id information, or the id is from an expired user, an error is returned.
 * If the id stored in the cookies is valid, then the request can continue, (<code>next()</code>).
 * @name ID Checking Middleware
 */
router.use( function( req, res, next ) {
	if ( !req.cookies || !req.cookies.userId ) {
		res.status( 401 ).send( {
			status: 'Session wasn\'t initialized. Send request to "/" first.'
		} );
	} else if ( !users.userExists( req.cookies.userId ) ) {
		res.status( 401 ).send( {
			status: 'User doesn\'t exist. Expired session. Please, send request to / first.'
		} );
	} else {
		next( );
	}
} );

/**
 * If the incoming request is a CORS request, the body is parsed to a JSON object.
 * @name CORS Parsing Middleware
 */
router.use( function( req, res, next ) {
	if ( req.headers[ 'content-type' ].includes( 'x-www-form-urlencoded' ) ) {
		req.body = qs.parse( req.body );
	}
	next( );
} );

/**
 * <strong>ENDPOINT.</strong><br/>
 * The <tt>/init</tt> endpoint allows us to initialize the whole system.<br/>
 * The setting information can be received through the request itself (<code><strong>settings</strong></code> parameter),
 *  or through a file, in which case the path to said file must be indicated in the request
 * (<code><strong>settingsPath</strong></code>). Please keep in mind that the route to the file starts at the root of the project
 * (<code>tot-system/...</code>).
 * @function /init
 */
router.post( '/init', function( req, res, next ) {
	console.log( '****************************INIT****************************' );
	const promises = [ ];
	let detectorsData = {};
	//qs.parse(req.body when request is url-encoded)
	if ( req.body.settings ) {
		console.log( req.body.settings );
		detectorsData = req.body.settings;
	} else if ( req.body.settingsFile ) {
		detectorsData = JSON.parse(
			fs.readFileSync( './' + req.body.settingsFile )
		);
	}
	//If there is data in detectorsData, we create the detectors
	if ( Object.keys( detectorsData ).length ) {
		const detectorHandler = new core.DetectorHandler( );
		for ( const detectorId in detectorsData ) {
			const callbacks = require( detectorsData[ detectorId ].callbacks );
			const newDetector = core.createDetector(
				detectorId,
				detectorsData[ detectorId ].category,
				detectorsData[ detectorId ].media,
				detectorsData[ detectorId ].realTime,
				detectorsData[ detectorId ].url,
				detectorsData[ detectorId ].otherOptions,
				callbacks.initialize,
				callbacks.extractEmotions,
				callbacks.translateToPAD
			);
			promises.push( newDetector.initialize( ) );
			detectorHandler.addDetector( newDetector );
		}
		Promise.all( promises ).then( function( results ) {
			users.setDetectorHandler( req.cookies.userId, detectorHandler );
			results.forEach( function( value, ...args ) {
				console.log( value );
			} );
			res.status( 200 ).send( {
				status: 'Detectors initialized',
				detectorNumber: results.length
			} );
		}, function( results ) {
			console.error( 'Something went horrible wrong' );
			res.status( 418 ).send( {
				status: 'Error on initialization'
			} );
		} );
	} else {
		//if detectorData is empty
		res.status( 400 ).send( {
			status: 'No detector data read'
		} );
	}
} );

/**
 * <strong>ENDPOINT.</strong><br/>
 * The <tt>/setup</tt> endpoint allows us to customize a little bit your set of detectores.
 * The request will recieve up to 3 parameters, all of them optional: <br/>
 * <ul>
 * 	<li><tt>type</tt>: Array of the detector categories you want to keep. Detector categories which are not in this array will be deteled.
 * 	An empty array deteles every category.</li>
 * 	<li><tt>realTime</tt>: Boolean which states if you want detectors which work in real time or not.</li>
 * 	<li><tt>delay</tt>: Upper threshold of the delay attribute. The delay attribute is set in the /initialize endpoint and represents the average time that
 * 	a certain detector needs to fulfil a request. Detectors whose delay attribute is bigger than the delay parameter will be deleted.</li>
 * </ul>
 * @function /setup
 */
router.post( '/setup', function( req, res, next ) {
	console.log( '****************************SETUP****************************' );
	const preferences = req.body;
	console.log( preferences );
	if ( Object.keys( preferences ).length !== 0 ) {
		try {
			const user = users.getUser( req.cookies.userId );
			const detectorsAffected = user.detectorHandler.setupDetectors( preferences );
			res.status( 200 ).send( {
				status: 'OK',
				detectorsAffected: detectorsAffected,
				detectorsUsed: user.detectorHandler.lengthDetectors( )
			} );
		} catch ( errorData ) {
			console.error( errorData );
			res.status( 400 ).send( {
				status: 'error',
				error: errorData
			} );
		}
	} else {
		res.status( 400 ).send( 'Preferences not set. Body request is empty. Every initial detector will be used' );
	}
} );

/**
 * <strong>ENDPOINT.</strong><br/>
 * The <tt>/analyse</tt> endpoint allows us to request for the analysis of some media file.
 * The request will recieve up to 3 parameters: <br/>
 * <ul>
 * 	<li><tt>mediaType</tt>: Kind of media which will be sent. Options can be "image", "video", "sound" and "text".</li>
 * 	<li><tt>lookingFor</tt>: Feature we want to analyse. Options can be "face", "voice", "signal" and "body".</li>
 * 	<li><tt>mediaPath</tt>: Absolute path to the file which contains the media. This can be a local path or an Internet address.</li>
 * </ul>
 * @function /analyse
 */
router.post( '/analyse', function( req, res, next ) {
	console.log( '****************************ANALYSE****************************' );
	const mediaInfo = req.body;
	if ( mediaInfo && !mediaInfo.mediaPath ) {
		res.status( 400 ).send( 'The request contais no path to media file. "mediaPath" attribute is missing' );
	} else {
		const analyseMedia = function( error, response, body ) {
			if ( error ) {
				res.status( 400 ).send( 'The media specified is not available either remotely or locally' );
			} else {
				try {
					users.getUser( req.cookies.userId ).detectorHandler
						.analyseMedia( mediaInfo.mediaType, mediaInfo.lookingFor, mediaInfo.mediaPath )
						.then( function( success ) {
							res.status( 200 ).send( 'Analyse started.' );
						} ).catch( function( error ) {
							console.log( error );
							res.status( 503 ).send( 'Detectors were not available/found' );
						} );
				} catch ( error ) {
					console.error( error );
					res.status( 400 ).send( {
						status: 'error',
						error: error
					} );
				}
			}
		};
		const fileIsLocal = fs.existsSync( mediaInfo.mediaPath );
		//We perform a HEAD request to check the file existence if the file is not local
		//If the HEAD request fails, the error is handled in the callback
		if ( !fileIsLocal ) {
			const options = {
				url: mediaInfo.mediaPath,
				method: 'HEAD'
			};
			request( options, analyseMedia );
		} else {
			analyseMedia( );
		}
	}
	//res.status( 400 ).send( 'Something went horribly wrong' );
	//We check if the file is in the system
} );

/**
 * <strong>ENDPOINT.</strong><br/>
 * The <code>/results</code> endpoint returns a single triplet as a result of aggregating
 * all the previous results from emotion detectors in the PAD format. The aggregation process has three
 * levels:
 * <ol>
 * 	<li>Each <code>detector</code> aggregates its results applying the <code>localStrategy</code> strategy,
 * 	turning a PAD results array into a single PAD triplet. At this point, we have a PAD triplet per detector.</li>
 * 	<li>These triplets are aggregated again, using the <code>localStrategy</code> strategy, but grouping them
 * 	by <strong>channel</strong>. At this point, we have a PAD triplet per emotion channel.</li>
 * 	<li>Finally, these channels' triplets are aggregated using the <code>globalStrategy</code> strategy,
 * 	producing the final PAD triplet which is returned to the user.</li>
 * </ol>
 * @function /results
 * @param {String|Array} [channelsToMerge] - Array of emotion channels to merge.
 * @param {string} localStrategy - Name of the strategy to use to aggregate the results of each individual
 *  detector and each channel.
 * @param {string} globalStrategy - Name of the strategy to use to aggregate the locally aggregated results.
 */
router.post( '/results', function( req, res, next ) {
	console.log( '****************************RESULTS****************************' );
	if ( !req.body.channelsToMerge ) {
		res.status( 400 ).send( {
			status: 'error',
			error: 'No channels especified, no fusion performed'
		} );
	} else {
		try {
			const mergedResults = users.getUser( req.cookies.userId ).detectorHandler.mergeResults(
				req.body.channelsToMerge,
				req.body.localStrategy,
				req.body.globalStrategy
			);
			res.status( 200 ).send( mergedResults );
		} catch ( error ) {
			console.error( error );
			res.status( 400 ).send( {
				status: 'error',
				error: error
			} );
		}
	}
} );

router.get( '/results/:channel/:type', function( req, res, next ) {
	console.log( '****************************RESULTS/CHANNEL****************************' );
	const preferences = req.body;
	const detectors = [ ];
	const detectorsInfo = fs.readFileSync( '../../credentials.json' );
	if ( preferences ) {

	} else {

	}
	res.status( 200 ).send( 'Todo ok' );
} );

router.get( '/results/:channel/:detector', function( req, res, next ) {
	console.log( '****************************RESULTS/CHANNEL/DETECTORS****************************' );
	if ( req.params.channel === void( 0 ) ) {
		const detector = detectorHandler.getDetectorFromChannel( );
	}
	res.status( 200 ).send( 'Todo ok' );
} );

/*
router.get( '/results-raw', function( req, res, next ) {
	const preferences = req.body;
	const detectors = [];
	const detectorsInfo = fs.readFileSync( '../../credentials.json' );
	if ( preferences ) {

	} else {

	}
	res.status( 200 ).send( config );
} );

router.get( '/results-raw/:channel', function( req, res, next ) {
	const preferences = req.body;
	const detectors = [];
	const detectorsInfo = fs.readFileSync( '../../credentials.json' );
	if ( preferences ) {

	} else {

	}
	res.status( 200 ).send( 'Todo ok' );
} );
*/
module.exports = router;