<commit_before>var
  redis = require("redis")
  , _ = require('underscore')._
  , neuron = require('neuron')
  , zlib = require('zlib')
  , winston = require('./logging').winston
  , oauth = require("./oauth")
  , oauth2 = require('./oauth2')
;

require('date-utils');

// Connect to redis
var client = redis.createClient(GLOBAL.config.redis.port, GLOBAL.config.redis.address);
if (GLOBAL.config.redis.auth) {
	client.auth(GLOBAL.config.redis.auth, function (err) {
		if (err) {
			// handle err;
		}
	});
}

//
// ## Recursive function to remove unwanted elements from API response
//
var filter = function( source, map) {
	if (_.isArray(source)) {
		_.each(source, function(item, index) { filter( item, map[0]); });
	} else {
		if (_.isString(source) || map === true || _.isUndefined(map)) return 0;
		_.each(source, function(obj, key, source) {
			if (_.isUndefined(map[key])) {
				delete source[key];
			} else {
				filter( obj, map[key]);
			}
		});
	};
}

//
// ## Function to send the response to the user
//
var sendResponse = function(jDoc, myRes, ip, bid, callback, gzip) {

	// Convert the string representation of date to a Date object
	jDoc.expires = new Date(jDoc.expires);
	jDoc.lastModified = new Date(jDoc.lastModified);

	// If there is a valid expiration date for the bundle
	if ( 'expires' in jDoc && _.isDate(jDoc.expires) ) {
		jDoc.secleft = jDoc.expires.getSecondsBetween( new Date() ) * -1;
	} else {
		// This should never happen
		jDoc.secleft = -1;
	}

	if (Number(jDoc.secleft) < 0 ) {

		// The bundle has expired. Force a refresh
		exports.fulfill( myRes, ip, bid, callback, gzip, true );

	} else {

		//Respond with the cached data

		var responseType = callback ? 'application/javascript' : 'application/json';

		var responseHeaders = {'Content-Type': responseType, 'vary': 'Accept-Encoding', 'max-age': jDoc.secleft, 'cache-control': 'public, max-age='+jDoc.secleft+', no-transform', "Expires": jDoc.expires, "Last-Modified": jDoc.lastModified};

		doc = JSON.stringify(jDoc);

		if (callback) {
			doc = callback + '(' + doc + ');';
		}

		if (gzip) {
			responseHeaders['content-encoding'] = 'gzip';
			zlib.gzip(doc, function(err, zbuf) {
			  if (!err) {
			    winston.event('Send gzipped response for ' + bid +', ' + zbuf.toString().length + ', ' + ip);
			    myRes.writeHead(200, responseHeaders);
			    myRes.end(zbuf);
			  }
			});
		} else {
			// If a callback name was passed, use it. Otherwise, just output the object
			var tbuf = new Buffer(doc);
			myRes.writeHead(200, responseHeaders);
			winston.event('Send response for ' + bid +', ' + doc.length + ', ' + ip);
			myRes.end(tbuf);
		}

	}

}

//
// ## Perform scheduled refresh
//
exports.refresh = function(api, key, bid, bundle) {

	winston.info('exports.refresh: ' + api);
	// We're forcing a refresh of the content so run the api.code
	api.resource( api.params, api.credentials, function( err, res ) {
		if ( err ) {

			// We got an error so set our output object to be the error and expire immediately
			api.expires = ( new Date() );
  			var tout = {
  				expires: api.expires,
  				result: err,
  				iid: bid+key,
  				cname: key,
  				scheduled: true
  			};

  			// Why are we doing this? Nothing happens here.

		} else {

  			winston.event('Get data for ' + bid + ' from ' + key + ', ' + res.size);

  			// Perform cleanup function on API response
  			if (_.has(api, 'cleanup')) {
	  			res = api.cleanup(res);
  			}

			// Filter the response
  			if (_.has(api, 'filter')) {
	  			filter ( res, api.filter );
  			}

			// Build the stored response
  			api.expires = ( new Date() ).addSeconds( api.cacheduration );
  			bundle[key] = api;

  			var tout = {
  				expires: api.expires,
  				result: res,
  				iid: api.iid,
  				cname: key,
  				scheduled: true
  			};

	   		// Save the API response to Redis
	   		client.set(bid+key, JSON.stringify(tout));

	   		// Delete the cached bundle. It will be rebuilt the next time a user requests it.
	   		client.del('bid'+bid);
		}
  	});

}

//
// ## Retrieve the requested bundle
//
exports.fulfill = function ( myRes, ip, bid, callback, gzip, override ) {

	winston.info('exports.fulfill: ' + bid);

	var bundle = GLOBAL.bundles[bid];

	// If the user requested a bundle that is not defined
	if (typeof bundle === 'undefined') {
		myRes.writeHead(404);
		myRes.end();
		return false;
	}

	if (!callback && bundle.callback) {
		callback = bundle.callback;
	}

	// Count the number of queries in this bundle so we know when we are ready to respond
	var queriesInThisBundle = _.size(bundle),
		thisResponse = {};

	// cleanup is not an API request
	if(_.has(bundle, 'cleanup')) {
		queriesInThisBundle--;
	}

	// callback is not an API request
	if(_.has(bundle, 'callback')) {
		queriesInThisBundle--;
	}

	// If override was not passed
	if( _.isUndefined( override )) {

		// Retrieve bundle response from Redis
		client.get('bid'+bid, function ( err, doc ) {

			if ( err || doc === null ) {
				// There was an error so force refresh on bundle
				exports.fulfill( myRes, ip, bid, callback, gzip, true );
			} else {
				jDoc = JSON.parse( doc );
				jDoc.fromcache = true;
				sendResponse(jDoc, myRes, ip, bid, callback, gzip);
			}
		});

	} else {

		// ### Override was passed so we are forcing a refresh on the bundle
		var manager = new neuron.JobManager();

		manager.addJob('fulfillPart', {
			work: function(api, bid, key, override, cachedPart) {

				winston.info('manager:fulfillPart: ' + bid + '.' + key + ', override: '+override);

				var self = this;

				if ( _.isUndefined( override ) ) {

					// Load the cached api response from Redis
			  		client.get(bid+key, function (err, doc) {
			  			if (err || doc === null){
			  				self.finished = true;
			  				manager.enqueue('fulfillPart', api, bid, key, true );
			  			} else {

				  			doc = JSON.parse( doc );
				  			doc.expires = new Date(doc.expires);
				  			if ( ('expires' in doc) && _.isDate(doc.expires) ) {
				  				var secleft = doc.expires.getSecondsBetween( new Date() ) * -1;
				  			}
				  			if (secleft < 0) {
				  				self.finished = true;
				  				manager.enqueue('fulfillPart', api, bid, key, true, doc );
				  			} else {
				  				doc.fromcache = true;
				  				manager.enqueue('finishRequest', doc );
				  				self.finished = true;
				  			}
				  		}
				  	});

				} else {

					if (_.has( api, 'auth')) {

						winston.info('Bundle uses auth type ' + api.auth.type);
						// If the API request object has an auth scheme defined
						if (api.auth.type == 'oauth') {
							oauth.authorize (api, bid, key, function( result, authParams ) {
								if (result === true) {
									api.params = _.extend(api.params, authParams);
									manager.enqueue('startRequest', api, key, cachedPart, bid);
								} else {
									manager.enqueue('finishRequest', result );
								}
								self.finished = true;
							});
						} else if (api.auth.type == 'oauth2') {
							oauth2.authorize (api, bid, key, function( result, authParams ) {
								if (result === true) {
									api.params = _.extend(api.params, authParams);
									manager.enqueue('startRequest', api, key, cachedPart, bid);
								} else {
									manager.enqueue('finishRequest', result );
								}
								self.finished = true;
							});
						} else {
							winston.error('auth type ' + api.auth.type + ' not recognized');
						}
					} else {
						// Authentication is not needed
						self.finished = true;
						manager.enqueue('startRequest', api, key, cachedPart, bid);
					}

				}

			}
		});

		manager.addJob('startRequest', {
			work: function( api, key, cachedPart, bid ) {

				winston.info('manager:startRequest: ' + key);

				var self = this;

				if (_.has( api, 'timeout') && _.isObject(cachedPart)) {
					self.timeout = setTimeout(function(self) {
						if(_.isObject(cachedPart)) {
							cachedPart.timeout = true;
							cachedPart.fromcache = true;
						} else {
							cachedPart= {
								"cname": key,
								"timeout" : true,
								"fromcache" : false
							};
						}
						manager.enqueue('finishRequest', cachedPart );
						self.finished = true;
					}, api.timeout, self)
				}

				api.resource( api.params, api.credentials, function( err, res ) {
		  			clearTimeout(self.timeout)
		  			delete self.timeout;

		  			if ( err ) {

		  				api.expires = ( new Date() );
			  			tout = {};
			  			tout.cname = key;
			  			tout.expires = api.expires;
			  			tout.result =  _.isUndefined(cachedPart) ? {} : cachedPart;
			  			tout.fromcache = true;
			  			tout.err = err;
			  			winston.error('Problem retrieving data for ' + bid + ' from ' + key + ': ' + JSON.stringify(err));

		  			} else {

			  			winston.event('Get data for ' + bid + ' from ' + key + ', ' + res.size);

			  			// Perform cleanup function on API response
			  			if (_.has(api, 'cleanup')) {
				  			res = api.cleanup(res);
			  			}

			  			// Filter the response
			  			if (_.has(api, 'filter')) {
				  			filter ( res, api.filter );
			  			}

			  			// Build the stored response
			  			api.expires = ( new Date() ).addSeconds( api.cacheduration );
			  			bundle[key] = api;
			  			//client.set('bundle'+bid, JSON.stringify(bundle));
			  			var tout = {
			  				expires: api.expires,
			  				result: res,
			  				iid: api.iid,
			  				cname: key
			  			};

				   		// Save the API response to Redis
				   		client.set(bid+key, JSON.stringify(tout));
				   	}
					manager.enqueue('finishRequest', tout );
	  				self.finished = true;
			  	});
			}
		})

		manager.addJob('finishRequest', {
			work: function(apiResponse) {

				winston.info('manager:finishRequest');

				queriesInThisBundle--;

				if (_.has(apiResponse, 'redirect')) thisResponse.redirect = apiResponse.redirect;
				thisResponse[apiResponse.cname] = apiResponse;

			  	if (queriesInThisBundle === 0) {
			  		manager.enqueue('composeResponse', bid);
			  	}
				this.finished = true;
			}
		});

		manager.addJob('composeResponse', {
			work: function() {

				winston.info('manager:composeResponse');

				// Update the expiration date on the bundle
				var tout = {
					expires: _.min( thisResponse, function( val ) { return val.expires } ).expires,
					lastModified: new Date()
				};

				if (_.has( thisResponse, 'redirect')) tout.redirect = thisResponse.redirect;

				// Insert api responses into bundle
				_.each( thisResponse, function( val, idx ) {
					tout[val.cname] = val;
				});

				// Perform cleanup function on bundle
			  	if (_.has(bundle, 'cleanup')) {
				  	tout = bundle.cleanup(tout);
				}

				// Save cached bundle in Redis
				client.set('bid'+bid, JSON.stringify(tout));

				// Determine the seconds left before expiry
				if ( 'expires' in tout && _.isDate(tout.expires) ) {
					tout.secleft = tout.expires.getSecondsBetween( new Date() ) * -1;
				} else {
					tout.secleft = 3600;
				}

				manager.enqueue('sendResponse', tout);
				this.finished = true;

			}
		});

		manager.addJob('sendResponse', {
			work: function(doc) {

				winston.info('manager:sendResponse');

				if (_.has(doc, 'redirect')) {
					myRes.statusCode = 302;
					myRes.setHeader("Location", doc.redirect);
					myRes.end();
					this.finished = true;
				} else {
					// Send the results
					sendResponse(doc, myRes, ip, bid, callback, gzip);
				}
			}
		});

		manager.addJob('fulfillBundle', {

			work:function() {
				var parts = [];
				_.each( bundle, function( api, key ) {
					if (key !== 'cleanup' && key !== 'callback') {
						if (_.isUndefined(api, 'credentials')) {
							api.credentials = {};
						}
						manager.enqueue('fulfillPart', api, bid, key);
					}
				});
				this.finished = true;
			}
		});

		manager.enqueue('fulfillBundle');
	}
}<commit_msg>Updates to support OAuth1.0A<commit_after>var
  redis = require("redis")
  , _ = require('underscore')._
  , neuron = require('neuron')
  , zlib = require('zlib')
  , winston = require('./logging').winston
  , oauth = require("./oauth")
  , oauth2 = require('./oauth2')
;

require('date-utils');

// Connect to redis
var client = redis.createClient(GLOBAL.config.redis.port, GLOBAL.config.redis.address);
if (GLOBAL.config.redis.auth) {
	client.auth(GLOBAL.config.redis.auth, function (err) {
		if (err) {
			// handle err;
		}
	});
}

//
// ## Recursive function to remove unwanted elements from API response
//
var filter = function( source, map) {
	if (_.isArray(source)) {
		_.each(source, function(item, index) { filter( item, map[0]); });
	} else {
		if (_.isString(source) || map === true || _.isUndefined(map)) return 0;
		_.each(source, function(obj, key, source) {
			if (_.isUndefined(map[key])) {
				delete source[key];
			} else {
				filter( obj, map[key]);
			}
		});
	};
}

//
// ## Function to send the response to the user
//
var sendResponse = function(jDoc, myRes, ip, bid, callback, gzip) {

	// Convert the string representation of date to a Date object
	jDoc.expires = new Date(jDoc.expires);
	jDoc.lastModified = new Date(jDoc.lastModified);

	// If there is a valid expiration date for the bundle
	if ( 'expires' in jDoc && _.isDate(jDoc.expires) ) {
		jDoc.secleft = jDoc.expires.getSecondsBetween( new Date() ) * -1;
	} else {
		// This should never happen
		jDoc.secleft = -1;
	}

	if (Number(jDoc.secleft) < 0 ) {

		// The bundle has expired. Force a refresh
		exports.fulfill( myRes, ip, bid, callback, gzip, true );

	} else {

		//Respond with the cached data

		var responseType = callback ? 'application/javascript' : 'application/json';

		var responseHeaders = {'Content-Type': responseType, 'vary': 'Accept-Encoding', 'max-age': jDoc.secleft, 'cache-control': 'public, max-age='+jDoc.secleft+', no-transform', "Expires": jDoc.expires, "Last-Modified": jDoc.lastModified};

		doc = JSON.stringify(jDoc);

		if (callback) {
			doc = callback + '(' + doc + ');';
		}

		if (gzip) {
			responseHeaders['content-encoding'] = 'gzip';
			zlib.gzip(doc, function(err, zbuf) {
			  if (!err) {
			    winston.event('Send gzipped response for ' + bid +', ' + zbuf.toString().length + ', ' + ip);
			    myRes.writeHead(200, responseHeaders);
			    myRes.end(zbuf);
			  }
			});
		} else {
			// If a callback name was passed, use it. Otherwise, just output the object
			var tbuf = new Buffer(doc);
			myRes.writeHead(200, responseHeaders);
			winston.event('Send response for ' + bid +', ' + doc.length + ', ' + ip);
			myRes.end(tbuf);
		}

	}

}

//
// ## Perform scheduled refresh
//
exports.refresh = function(api, key, bid, bundle) {

	winston.info('exports.refresh: ' + api);
	// We're forcing a refresh of the content so run the api.code
	api.resource( api.params, api.credentials, function( err, res ) {
		if ( err ) {

			// We got an error so set our output object to be the error and expire immediately
			api.expires = ( new Date() );
  			var tout = {
  				expires: api.expires,
  				result: err,
  				iid: bid+key,
  				cname: key,
  				scheduled: true
  			};

  			// Why are we doing this? Nothing happens here.

		} else {

  			winston.event('Get data for ' + bid + ' from ' + key + ', ' + res.size);

  			// Perform cleanup function on API response
  			if (_.has(api, 'cleanup')) {
	  			res = api.cleanup(res);
  			}

			// Filter the response
  			if (_.has(api, 'filter')) {
	  			filter ( res, api.filter );
  			}

			// Build the stored response
  			api.expires = ( new Date() ).addSeconds( api.cacheduration );
  			bundle[key] = api;

  			var tout = {
  				expires: api.expires,
  				result: res,
  				iid: api.iid,
  				cname: key,
  				scheduled: true
  			};

	   		// Save the API response to Redis
	   		client.set(bid+key, JSON.stringify(tout));

	   		// Delete the cached bundle. It will be rebuilt the next time a user requests it.
	   		client.del('bid'+bid);
		}
  	});

}

//
// ## Retrieve the requested bundle
//
exports.fulfill = function ( myRes, ip, bid, callback, gzip, override ) {

	winston.info('exports.fulfill: ' + bid);

	var bundle = GLOBAL.bundles[bid];

	// If the user requested a bundle that is not defined
	if (typeof bundle === 'undefined') {
		myRes.writeHead(404);
		myRes.end();
		return false;
	}

	if (!callback && bundle.callback) {
		callback = bundle.callback;
	}

	// Count the number of queries in this bundle so we know when we are ready to respond
	var queriesInThisBundle = _.size(bundle),
		thisResponse = {};

	// cleanup is not an API request
	if(_.has(bundle, 'cleanup')) {
		queriesInThisBundle--;
	}

	// callback is not an API request
	if(_.has(bundle, 'callback')) {
		queriesInThisBundle--;
	}

	// If override was not passed
	if( _.isUndefined( override )) {

		// Retrieve bundle response from Redis
		client.get('bid'+bid, function ( err, doc ) {

			if ( err || doc === null ) {
				// There was an error so force refresh on bundle
				exports.fulfill( myRes, ip, bid, callback, gzip, true );
			} else {
				jDoc = JSON.parse( doc );
				jDoc.fromcache = true;
				sendResponse(jDoc, myRes, ip, bid, callback, gzip);
			}
		});

	} else {

		// ### Override was passed so we are forcing a refresh on the bundle
		var manager = new neuron.JobManager();

		manager.addJob('fulfillPart', {
			work: function(api, bid, key, override, cachedPart) {

				winston.info('manager:fulfillPart: ' + bid + '.' + key + ', override: '+override);

				var self = this;

				if ( _.isUndefined( override ) ) {

					// Load the cached api response from Redis
			  		client.get(bid+key, function (err, doc) {
			  			if (err || doc === null){
			  				self.finished = true;
			  				manager.enqueue('fulfillPart', api, bid, key, true );
			  			} else {

				  			doc = JSON.parse( doc );
				  			doc.expires = new Date(doc.expires);
				  			if ( ('expires' in doc) && _.isDate(doc.expires) ) {
				  				var secleft = doc.expires.getSecondsBetween( new Date() ) * -1;
				  			}
				  			if (secleft < 0) {
				  				self.finished = true;
				  				manager.enqueue('fulfillPart', api, bid, key, true, doc );
				  			} else {
				  				doc.fromcache = true;
				  				manager.enqueue('finishRequest', doc );
				  				self.finished = true;
				  			}
				  		}
				  	});

				} else {

					if (_.has( api, 'auth')) {

						winston.info('Bundle uses auth type ' + api.auth.type);
						// If the API request object has an auth scheme defined
						if (api.auth.type == 'oauth') {
							oauth.authorize (api, bid, key, function( result, authParams ) {
								if (result === true) {
									api.params = _.extend(api.params, authParams);
									manager.enqueue('startRequest', api, key, cachedPart, bid);
								} else {
									manager.enqueue('finishRequest', result );
								}
								self.finished = true;
							});
						} else if (api.auth.type == 'oauth2') {
							oauth2.authorize (api, bid, key, function( result, authParams ) {
								if (result === true) {
									api.params = _.extend(api.params, authParams);
									manager.enqueue('startRequest', api, key, cachedPart, bid);
								} else {
									manager.enqueue('finishRequest', result );
								}
								self.finished = true;
							});
						} else {
							winston.error('auth type ' + api.auth.type + ' not recognized');
						}
					} else {
						// Authentication is not needed
						self.finished = true;
						manager.enqueue('startRequest', api, key, cachedPart, bid);
					}

				}

			}
		});

		manager.addJob('startRequest', {
			work: function( api, key, cachedPart, bid ) {

				winston.info('manager:startRequest: ' + key);

				var self = this;

				if (_.has( api, 'timeout') && _.isObject(cachedPart)) {
					self.timeout = setTimeout(function(self) {
						if(_.isObject(cachedPart)) {
							cachedPart.timeout = true;
							cachedPart.fromcache = true;
						} else {
							cachedPart= {
								"cname": key,
								"timeout" : true,
								"fromcache" : false
							};
						}
						manager.enqueue('finishRequest', cachedPart );
						self.finished = true;
					}, api.timeout, self)
				}

				api.resource( api.params, api.credentials, function( err, res ) {
		  			clearTimeout(self.timeout)
		  			delete self.timeout;

		  			if ( err ) {

		  				api.expires = ( new Date() );
			  			tout = {};
			  			tout.cname = key;
			  			tout.expires = api.expires;
			  			tout.result =  _.isUndefined(cachedPart) ? {} : cachedPart;
			  			tout.fromcache = true;
			  			tout.err = err;
			  			winston.error('Problem retrieving data for ' + bid + ' from ' + key + ': ' + JSON.stringify(err));

		  			} else {

			  			winston.event('Get data for ' + bid + ' from ' + key + ', ' + res.size);

			  			// Perform cleanup function on API response
			  			if (_.has(api, 'cleanup')) {
				  			res = api.cleanup(res);
			  			}

			  			// Filter the response
			  			if (_.has(api, 'filter')) {
				  			filter ( res, api.filter );
			  			}

			  			// Build the stored response
			  			api.expires = ( new Date() ).addSeconds( api.cacheduration );
			  			bundle[key] = api;
			  			//client.set('bundle'+bid, JSON.stringify(bundle));
			  			var tout = {
			  				expires: api.expires,
			  				result: res,
			  				iid: api.iid,
			  				cname: key
			  			};

				   		// Save the API response to Redis
				   		client.set(bid+key, JSON.stringify(tout));
				   	}
					manager.enqueue('finishRequest', tout );
	  				self.finished = true;
			  	});
			}
		})

		manager.addJob('finishRequest', {
			work: function(apiResponse) {

				winston.info('manager:finishRequest');

				queriesInThisBundle--;

				if (_.has(apiResponse, 'redirect')) {
					thisResponse["redirect"] = apiResponse.redirect;
					thisResponse["guid"] = apiResponse.guid;
					thisResponse["authBundle"] = bid;
					thisResponse["authPart"] = apiResponse.cname;
				}
				thisResponse[apiResponse.cname] = apiResponse;

			  	if (queriesInThisBundle === 0) {
			  		manager.enqueue('composeResponse', bid);
			  	}
				this.finished = true;
			}
		});

		manager.addJob('composeResponse', {
			work: function() {

				winston.info('manager:composeResponse');

				// Update the expiration date on the bundle
				var tout = {
					expires: _.min( thisResponse, function( val ) { return val.expires } ).expires,
					lastModified: new Date()
				};

				if (_.has( thisResponse, 'redirect')) {
					tout.redirect = thisResponse.redirect,
					tout.guid = thisResponse.guid,
					tout.authBundle = thisResponse.authBundle,
					tout.authPart = thisResponse.authPart
				};

				// Insert api responses into bundle
				_.each( thisResponse, function( val, idx ) {
					tout[val.cname] = val;
				});

				// Perform cleanup function on bundle
			  	if (_.has(bundle, 'cleanup')) {
				  	tout = bundle.cleanup(tout);
				}

				// Save cached bundle in Redis
				client.set('bid'+bid, JSON.stringify(tout));

				// Determine the seconds left before expiry
				if ( 'expires' in tout && _.isDate(tout.expires) ) {
					tout.secleft = tout.expires.getSecondsBetween( new Date() ) * -1;
				} else {
					tout.secleft = 3600;
				}

				manager.enqueue('sendResponse', tout);
				this.finished = true;

			}
		});

		manager.addJob('sendResponse', {
			work: function(doc) {

				winston.info('manager:sendResponse');

				if (_.has(doc, 'redirect')) {

					if (_.has(doc, 'guid')) {
						GLOBAL.config.guids[doc.guid] = doc.authBundle+','+doc.authPart;
						myRes.setHeader("Set-Cookie", "authCode="+doc.guid);
						myRes.statusCode = 200;
						myRes.end('<p>Please authorize spas at <a href="'+doc.redirect+'">'+doc.redirect+'</a></p>');
						this.finished = true;
					} else {
						myRes.statusCode = 302;
						myRes.setHeader("Location", doc.redirect);
						myRes.end();
						this.finished = true;
					}
				} else {
					// Send the results
					sendResponse(doc, myRes, ip, bid, callback, gzip);
				}
			}
		});

		manager.addJob('fulfillBundle', {

			work:function() {
				var parts = [];
				_.each( bundle, function( api, key ) {
					if (key !== 'cleanup' && key !== 'callback') {
						if (_.isUndefined(api, 'credentials')) {
							api.credentials = {};
						}
						manager.enqueue('fulfillPart', api, bid, key);
					}
				});
				this.finished = true;
			}
		});

		manager.enqueue('fulfillBundle');
	}
}<|endoftext|>


<commit_before>'use strict';

var _ = require('underscore');

function ucfirst(property) {
	return [property.charAt(0).toUpperCase(), property.substr(1)].join('');
}

function lcfirst(property) {
	return [property.charAt(0).toLowerCase(), property.substr(1)].join('');
}

function plucker(data, property) {
	return _.object(_.keys(data), _.pluck(data, property));
}

function Entity(namespace, schema, defaults) {

	var object = {},
		data = {},
		setters = {},
		getters = {},
		validators = {};

	_.each(schema, function (value, key) {

		if (_.isFunction(value)) {
			if (key.substr(0, 3) === 'set') {
				setters[lcfirst(key.substr(3))] = value;
				return null;
			}
			if (key.substr(0, 3) === 'get') {
				getters[lcfirst(key.substr(3))] = value;
				return null;
			}
			if (key.substr(0, 8) === 'validate') {
				validators[lcfirst(key.substr(8))] = value;
				return null;
			}
		} else {
			data[key] = {
				initial: value,
				modified: false,
				value: value
			};
			object['get' + ucfirst(key)] = function () {
				return getKey(key);
			};
			object['set' + ucfirst(key)] = function (value) {
				return setKey(key, value);
			};
			return data[key];
		}

		return null;
	});

	function set(values) {
		_.each(values, function (value, key) {
			return setKey(key, value, values);
		});
	}

	function get() {
		return _.object(_.keys(data), _.map(_.keys(data), getKey));
	}

	function setKey(key, value, values) {
		if (_.isUndefined(values)) {
			values = {};
			values[key] = value;
		}
		if (!_.has(schema, key)) {
			return;
			// throw new Error('Setting undefined property'); // Too much strictness
		}
		if (_.has(validators, key) && !validators[key](value, values, data)) {
			throw new Error('Value [' + value + '] is not valid for [' + namespace + '.' + key + ']');
		}
		if (_.has(setters, key)) {
			value = setters[key](value, values, get());
		}
		data[key].value = value;
		data[key].modified = true;
	}

	function getKey(key) {
		if (!_.has(data, key)) {
			throw new Error('Accessing undefined property');
		}
		if (_.has(getters, key) && _.isEmpty(data[key].value)) {
			return getters[key](plucker(data, 'value'), object);
		}
		return data[key].value;
	}

	if (defaults !== undefined) {
		set(defaults);
	}

	object.set = set;
	object.get = get;
	object.initial = function () {
		return plucker(data, 'initial');
	};
	object.modified = function () {
		return plucker(data, 'modified');
	};
	object.is = function (ns) {
		return ns === namespace;
	};
	object.toJSON = function () {
		var rawData = get();
		rawData.__ns = namespace;
		return rawData;
	};

	return object;
}

exports.Entity = Entity;<commit_msg>Native toString override for gleams<commit_after>'use strict';

var _ = require('underscore');

function ucfirst(property) {
	return [property.charAt(0).toUpperCase(), property.substr(1)].join('');
}

function lcfirst(property) {
	return [property.charAt(0).toLowerCase(), property.substr(1)].join('');
}

function plucker(data, property) {
	return _.object(_.keys(data), _.pluck(data, property));
}

function Entity(namespace, schema, defaults) {

	var object = {},
		data = {},
		setters = {},
		getters = {},
		validators = {};

	_.each(schema, function (value, key) {

		if (_.isFunction(value)) {
			if (key.substr(0, 3) === 'set') {
				setters[lcfirst(key.substr(3))] = value;
				return null;
			}
			if (key.substr(0, 3) === 'get') {
				getters[lcfirst(key.substr(3))] = value;
				return null;
			}
			if (key.substr(0, 8) === 'validate') {
				validators[lcfirst(key.substr(8))] = value;
				return null;
			}
		} else {
			data[key] = {
				initial: value,
				modified: false,
				value: value
			};
			object['get' + ucfirst(key)] = function () {
				return getKey(key);
			};
			object['set' + ucfirst(key)] = function (value) {
				return setKey(key, value);
			};
			return data[key];
		}

		return null;
	});

	function set(values) {
		_.each(values, function (value, key) {
			return setKey(key, value, values);
		});
	}

	function get() {
		return _.object(_.keys(data), _.map(_.keys(data), getKey));
	}

	function setKey(key, value, values) {
		if (_.isUndefined(values)) {
			values = {};
			values[key] = value;
		}
		if (!_.has(schema, key)) {
			return;
			// throw new Error('Setting undefined property'); // Too much strictness
		}
		if (_.has(validators, key) && !validators[key](value, values, data)) {
			throw new Error('Value [' + value + '] is not valid for [' + namespace + '.' + key + ']');
		}
		if (_.has(setters, key)) {
			value = setters[key](value, values, get());
		}
		data[key].value = value;
		data[key].modified = true;
	}

	function getKey(key) {
		if (!_.has(data, key)) {
			throw new Error('Accessing undefined property');
		}
		if (_.has(getters, key) && _.isEmpty(data[key].value)) {
			return getters[key](plucker(data, 'value'), object);
		}
		return data[key].value;
	}

	if (defaults !== undefined) {
		set(defaults);
	}

	object.set = set;
	object.get = get;
	object.initial = function () {
		return plucker(data, 'initial');
	};
	object.modified = function () {
		return plucker(data, 'modified');
	};
	object.is = function (ns) {
		return ns === namespace;
	};
	object.toJSON = function () {
		var rawData = get();
		rawData.__ns = namespace;
		return rawData;
	};
	object.toString = function () {
		return ['[', 'object', ' ', 'Gleam:', namespace, ']'].join('');
	};

	return object;
}

exports.Entity = Entity;<|endoftext|>


<commit_before>'use strict';

/**
 * Sequelize provides a host of custom error classes, to allow you to do easier debugging. All of these errors are exposed on the sequelize object and the sequelize constructor.
 * All sequelize errors inherit from the base JS error object.
 *
 * @fileOverview The Error Objects produced by Sequelize.
 * @class Errors
 */

var util = require('util');
var error = module.exports = {};

/**
 * The Base Error all Sequelize Errors inherit from.
 *
 * @constructor
 * @alias Error
 */
error.BaseError = function() {
  var tmp = Error.apply(this, arguments);
  tmp.name = this.name = 'SequelizeBaseError';

  Error.captureStackTrace && Error.captureStackTrace(this, this.constructor);
  this.message = tmp.message;
};
util.inherits(error.BaseError, Error);

/**
 * Validation Error. Thrown when the sequelize validation has failed. The error contains an `errors` property,
 * which is an array with 1 or more ValidationErrorItems, one for each validation that failed.
 *
 * @param {string} message Error message
 * @param {Array} [errors] Array of ValidationErrorItem objects describing the validation errors
 *
 * @extends BaseError
 * @constructor
 */
error.ValidationError = function(message, errors) {
  error.BaseError.apply(this, arguments);
  this.name = 'SequelizeValidationError';
  this.errors = errors || [];
};
util.inherits(error.ValidationError, error.BaseError);

/**
 * Gets all validation error items for the path / field specified.
 *
 * @param {string} path The path to be checked for error items
 * @returns {Array} Validation error items for the specified path
 */
error.ValidationError.prototype.get = function(path) {
  return this.errors.reduce(function(reduced, error) {
    if (error.path === path) {
      reduced.push(error);
    }
    return reduced;
  }, []);
};

/**
 * An array of ValidationErrorItems
 * @property errors
 * @name errors
 */
error.ValidationError.prototype.errors;

/**
 * A base class for all database related errors.
 * @extends BaseError
 * @constructor
 */
error.DatabaseError = function (parent) {
  error.BaseError.apply(this, [parent.message]);
  this.name = 'SequelizeDatabaseError';

  this.parent = parent;
  this.sql = parent.sql;
};
util.inherits(error.DatabaseError, error.BaseError);

/**
 * The database specific error which triggered this one
 * @property parent
 * @name parent
 */
error.DatabaseError.prototype.parent;
/**
 * The SQL that triggered the error
 * @property sql
 * @name sql
 */
error.DatabaseError.prototype.sql;

/**
 * A base class for all connection related errors.
 * @extends BaseError
 * @constructor
 */
error.ConnectionError = function (parent) {
  error.BaseError.apply(this, [parent.message]);
  this.name = 'SequelizeConnectionError';

  this.parent = parent;
};
util.inherits(error.ConnectionError, error.BaseError);

/**
 * The connection specific error which triggered this one
 * @property parent
 * @name parent
 */
error.ConnectionError.prototype.parent;

/**
 * Thrown when a connection to a database is refused
 * @extends ConnectionError
 * @constructor
 */
error.ConnectionRefusedError = function (parent) {
  error.ConnectionError.call(this, parent);
  this.name = 'SequelizeConnectionRefusedError';
};
util.inherits(error.ConnectionRefusedError, error.ConnectionError);

/**
 * Thrown when a connection to a database is refused due to insufficient privileges
 * @extends ConnectionError
 * @constructor
 */
error.AccessDeniedError = function (parent) {
  error.AccessDeniedError.call(this, parent);
  this.name = 'SequelizeAccessDeniedError';
};
util.inherits(error.AccessDeniedError, error.ConnectionError);

/**
 * Thrown when a connection to a database has a hostname that was not found
 * @extends ConnectionError
 * @constructor
 */
error.HostNotFoundError = function (parent) {
  error.HostNotFoundError.call(this, parent);
  this.name = 'SequelizeHostNotFoundError';
};
util.inherits(error.HostNotFoundError, error.ConnectionError);

/**
 * Thrown when a connection to a database has a hostname that was not reachable
 * @extends ConnectionError
 * @constructor
 */
error.HostNotReachableError = function (parent) {
  error.HostNotReachableError.call(this, parent);
  this.name = 'SequelizeHostNotReachableError';
};
util.inherits(error.HostNotReachableError, error.ConnectionError);

/**
 * Thrown when a connection to a database has invalid values for any of the connection parameters
 * @extends ConnectionError
 * @constructor
 */
error.InvalidConnectionError = function (parent) {
  error.InvalidConnectionError.call(this, parent);
  this.name = 'SequelizeInvalidConnectionError';
};
util.inherits(error.InvalidConnectionError, error.ConnectionError);

/**
 * Thrown when a connection to a database times out
 * @extends ConnectionError
 * @constructor
 */
error.ConnectionTimedOutError = function (parent) {
  error.ConnectionTimedOutError.call(this, parent);
  this.name = 'SequelizeConnectionTimedOutError';
};
util.inherits(error.ConnectionTimedOutError, error.ConnectionError);

/**
 * Thrown when a database query times out because of a deadlock
 * @extends DatabaseError
 * @constructor
 */
error.TimeoutError = function (parent) {
  error.DatabaseError.call(this, parent);
  this.name = 'SequelizeTimeoutError';
};
util.inherits(error.TimeoutError, error.BaseError);

/**
 * Thrown when a unique constraint is violated in the database
 * @extends DatabaseError
 * @constructor
 */
error.UniqueConstraintError = function (options) {
  options = options || {};
  options.parent = options.parent || { sql: '' };

  error.DatabaseError.call(this, options.parent);
  this.name = 'SequelizeUniqueConstraintError';

  this.message = options.message;
  this.fields = options.fields;
  this.value = options.value;
  this.index = options.index;
};
util.inherits(error.UniqueConstraintError, error.DatabaseError);

/**
 * Thrown when a foreign key constraint is violated in the database
 * @extends DatabaseError
 * @constructor
 */
error.ForeignKeyConstraintError = function (options) {
  options = options || {};
  options.parent = options.parent || { sql: '' };

  error.DatabaseError.call(this, options.parent);
  this.name = 'SequelizeForeignKeyConstraintError';

  this.message = options.message;
  this.fields = options.fields;
  this.table = options.table;
  this.value = options.value;
  this.index = options.index;
};
util.inherits(error.ForeignKeyConstraintError, error.DatabaseError);

/**
 * The message from the DB.
 * @property message
 * @name message
 */
error.DatabaseError.prototype.message;
/**
 * The fields of the unique constraint
 * @property fields
 * @name fields
 */
error.DatabaseError.prototype.fields;
/**
 * The value(s) which triggered the error
 * @property value
 * @name value
 */
error.DatabaseError.prototype.value;
/**
 * The name of the index that triggered the error
 * @property index
 * @name index
 */
error.DatabaseError.prototype.index;

/**
 * Validation Error Item
 * Instances of this class are included in the `ValidationError.errors` property.
 *
 * @param {string} message An error message
 * @param {string} type The type of the validation error
 * @param {string} path The field that triggered the validation error
 * @param {string} value The value that generated the error
 * @constructor
 */
error.ValidationErrorItem = function(message, type, path, value) {
  this.message = message || '';
  this.type = type || null;
  this.path = path || null;
  this.value = value || null;
};
<commit_msg>Moved the connection errors to after the Database and Validation errors so they aren't breaking up the grouping.<commit_after>'use strict';

/**
 * Sequelize provides a host of custom error classes, to allow you to do easier debugging. All of these errors are exposed on the sequelize object and the sequelize constructor.
 * All sequelize errors inherit from the base JS error object.
 *
 * @fileOverview The Error Objects produced by Sequelize.
 * @class Errors
 */

var util = require('util');
var error = module.exports = {};

/**
 * The Base Error all Sequelize Errors inherit from.
 *
 * @constructor
 * @alias Error
 */
error.BaseError = function() {
  var tmp = Error.apply(this, arguments);
  tmp.name = this.name = 'SequelizeBaseError';

  Error.captureStackTrace && Error.captureStackTrace(this, this.constructor);
  this.message = tmp.message;
};
util.inherits(error.BaseError, Error);

/**
 * Validation Error. Thrown when the sequelize validation has failed. The error contains an `errors` property,
 * which is an array with 1 or more ValidationErrorItems, one for each validation that failed.
 *
 * @param {string} message Error message
 * @param {Array} [errors] Array of ValidationErrorItem objects describing the validation errors
 *
 * @extends BaseError
 * @constructor
 */
error.ValidationError = function(message, errors) {
  error.BaseError.apply(this, arguments);
  this.name = 'SequelizeValidationError';
  this.errors = errors || [];
};
util.inherits(error.ValidationError, error.BaseError);

/**
 * Gets all validation error items for the path / field specified.
 *
 * @param {string} path The path to be checked for error items
 * @returns {Array} Validation error items for the specified path
 */
error.ValidationError.prototype.get = function(path) {
  return this.errors.reduce(function(reduced, error) {
    if (error.path === path) {
      reduced.push(error);
    }
    return reduced;
  }, []);
};

/**
 * An array of ValidationErrorItems
 * @property errors
 * @name errors
 */
error.ValidationError.prototype.errors;

/**
 * A base class for all database related errors.
 * @extends BaseError
 * @constructor
 */
error.DatabaseError = function (parent) {
  error.BaseError.apply(this, [parent.message]);
  this.name = 'SequelizeDatabaseError';

  this.parent = parent;
  this.sql = parent.sql;
};
util.inherits(error.DatabaseError, error.BaseError);

/**
 * The database specific error which triggered this one
 * @property parent
 * @name parent
 */
error.DatabaseError.prototype.parent;
/**
 * The SQL that triggered the error
 * @property sql
 * @name sql
 */
error.DatabaseError.prototype.sql;

/**
 * Thrown when a database query times out because of a deadlock
 * @extends DatabaseError
 * @constructor
 */
error.TimeoutError = function (parent) {
  error.DatabaseError.call(this, parent);
  this.name = 'SequelizeTimeoutError';
};
util.inherits(error.TimeoutError, error.BaseError);

/**
 * Thrown when a unique constraint is violated in the database
 * @extends DatabaseError
 * @constructor
 */
error.UniqueConstraintError = function (options) {
  options = options || {};
  options.parent = options.parent || { sql: '' };

  error.DatabaseError.call(this, options.parent);
  this.name = 'SequelizeUniqueConstraintError';

  this.message = options.message;
  this.fields = options.fields;
  this.value = options.value;
  this.index = options.index;
};
util.inherits(error.UniqueConstraintError, error.DatabaseError);

/**
 * Thrown when a foreign key constraint is violated in the database
 * @extends DatabaseError
 * @constructor
 */
error.ForeignKeyConstraintError = function (options) {
  options = options || {};
  options.parent = options.parent || { sql: '' };

  error.DatabaseError.call(this, options.parent);
  this.name = 'SequelizeForeignKeyConstraintError';

  this.message = options.message;
  this.fields = options.fields;
  this.table = options.table;
  this.value = options.value;
  this.index = options.index;
};
util.inherits(error.ForeignKeyConstraintError, error.DatabaseError);

/**
 * The message from the DB.
 * @property message
 * @name message
 */
error.DatabaseError.prototype.message;
/**
 * The fields of the unique constraint
 * @property fields
 * @name fields
 */
error.DatabaseError.prototype.fields;
/**
 * The value(s) which triggered the error
 * @property value
 * @name value
 */
error.DatabaseError.prototype.value;
/**
 * The name of the index that triggered the error
 * @property index
 * @name index
 */
error.DatabaseError.prototype.index;

/**
 * Validation Error Item
 * Instances of this class are included in the `ValidationError.errors` property.
 *
 * @param {string} message An error message
 * @param {string} type The type of the validation error
 * @param {string} path The field that triggered the validation error
 * @param {string} value The value that generated the error
 * @constructor
 */
error.ValidationErrorItem = function(message, type, path, value) {
  this.message = message || '';
  this.type = type || null;
  this.path = path || null;
  this.value = value || null;
};

/**
 * A base class for all connection related errors.
 * @extends BaseError
 * @constructor
 */
error.ConnectionError = function (parent) {
  error.BaseError.apply(this, [parent.message]);
  this.name = 'SequelizeConnectionError';

  this.parent = parent;
};
util.inherits(error.ConnectionError, error.BaseError);

/**
 * The connection specific error which triggered this one
 * @property parent
 * @name parent
 */
error.ConnectionError.prototype.parent;

/**
 * Thrown when a connection to a database is refused
 * @extends ConnectionError
 * @constructor
 */
error.ConnectionRefusedError = function (parent) {
  error.ConnectionError.call(this, parent);
  this.name = 'SequelizeConnectionRefusedError';
};
util.inherits(error.ConnectionRefusedError, error.ConnectionError);

/**
 * Thrown when a connection to a database is refused due to insufficient privileges
 * @extends ConnectionError
 * @constructor
 */
error.AccessDeniedError = function (parent) {
  error.AccessDeniedError.call(this, parent);
  this.name = 'SequelizeAccessDeniedError';
};
util.inherits(error.AccessDeniedError, error.ConnectionError);

/**
 * Thrown when a connection to a database has a hostname that was not found
 * @extends ConnectionError
 * @constructor
 */
error.HostNotFoundError = function (parent) {
  error.HostNotFoundError.call(this, parent);
  this.name = 'SequelizeHostNotFoundError';
};
util.inherits(error.HostNotFoundError, error.ConnectionError);

/**
 * Thrown when a connection to a database has a hostname that was not reachable
 * @extends ConnectionError
 * @constructor
 */
error.HostNotReachableError = function (parent) {
  error.HostNotReachableError.call(this, parent);
  this.name = 'SequelizeHostNotReachableError';
};
util.inherits(error.HostNotReachableError, error.ConnectionError);

/**
 * Thrown when a connection to a database has invalid values for any of the connection parameters
 * @extends ConnectionError
 * @constructor
 */
error.InvalidConnectionError = function (parent) {
  error.InvalidConnectionError.call(this, parent);
  this.name = 'SequelizeInvalidConnectionError';
};
util.inherits(error.InvalidConnectionError, error.ConnectionError);

/**
 * Thrown when a connection to a database times out
 * @extends ConnectionError
 * @constructor
 */
error.ConnectionTimedOutError = function (parent) {
  error.ConnectionTimedOutError.call(this, parent);
  this.name = 'SequelizeConnectionTimedOutError';
};
util.inherits(error.ConnectionTimedOutError, error.ConnectionError);
<|endoftext|>


<commit_before>var _ = require('lodash'),
		util = require('util'),
		retryableNetworkErrorCodes,

		// Marketo error codes
		// http://developers.marketo.com/documentation/rest/error-codes/
		mkCode = {
			TOKEN_EMPTY: '600',
			TOKEN_INVALID: '601',
			TOKEN_EXPIRED: '602',
			ACCESS_DENIED: '603',
			TIMED_OUT: '604',
			HTTP_NOT_SUPPORTED: '605',
			RATE_LIMIT_REACHED: '606',
			QUOTA_REACHED: '607',
			API_UNAVAILBLE: '608',
			INVALID_JSON: '609',
			RESOURCE_NOT_FOUND: '610',
			SYSTEM_ERROR: '611',
			INVALID_CONTENT_TYPE: '612',

			INVALID_VALUE: '1001',
			MISSING_REQUIRED_VALUE: '1002',
			INVALID_DATA: '1003',
			LEAD_NOT_FOUND: '1004',
			LEAD_ALREADY_EXISTS: '1005',
			FIELD_NOT_FOUND: '1006',
			MULTIPLE_MATCHING_LEADS: '1007',
			PARTITION_ACCESS_DENIED: '1008',
			PARTITION_UNSPECIFIED: '1009',
			PARTITION_UPDATE_NOT_ALLOWED: '1010',
			FIELD_UNSUPPORTED: '1011',
			INVALID_COOKIE: '1012',
			OBJECT_NOT_FOUND: '1013',
			OBJECT_CREATE_FAILED: '1014'
		};

// These error codes were pulled from
// https://github.com/joyent/node/blob/857975d5e7e0d7bf38577db0478d9e5ede79922e/deps/uv/include/uv-errno.h
// Is there a better way to do this?
retryableNetworkErrorCodes = [
	'EADDRINFO',
	'EADDRNOTAVAIL',
	'ECONNRESET',
	'ENOTFOUND',
	'ECONNABORTED',
	'EPROTO',
	'ECONNREFUSED',
	'EHOSTUNREACH',
	'ENETDOWN',
	'ENETUNREACH',
	'ENONET',
	'ENOTCONN',
	'ENOTSOCK',
	'ETIMEDOUT'
];

// for the record, here's the format of the Marketo data in case of error:
// {
//   requestId: '15381#161d91d807a',
//   success: false,
//   errors: [ { code: '605', message: 'Request method \'POST\' not supported' } ]
//  }

function getFirstMessage(data) {
	return data && data.errors ? data.errors[0] : 'Unknown Marketo error';
}

function Http5XXError(statusCode, data) {
	Error.call(this);
	this.message = getFirstMessage(data);
	this.data = data;
	this.code = statusCode;
}
util.inherits(Http5XXError, Error);

function Http4XXError(statusCode, data) {
	Error.call(this);
	this.message = getFirstMessage(data);
	this.data = data;
	this.code = statusCode;
}
util.inherits(Http4XXError, Error);

function TimeoutError(timeout) {
	Error.call(this, 'Timed out after ' + timeout + ' ms.');
}
util.inherits(TimeoutError, Error);

function hasMarketoCode(err, code) {
	return isMarketoError(err) &&
				 !!_.find(err.errors, {code: code});
}

function isMarketoError(err) {
	var val = _.has(err, 'requestId') &&
						_.isArray(err.errors) &&
						err.errors.length > 0;
	return val;
}

module.exports = {
	Http5XXError: Http5XXError,
	Http4XXError: Http4XXError,
	isMarketoError: isMarketoError,
	hasMarketoCode: hasMarketoCode,
	marketoErrorCodes: mkCode,

	isNetworkError: function(err) {
		var code = err.code || '';
		return retryableNetworkErrorCodes.indexOf(code) > -1 ||
					 (err instanceof TimeoutError);
	},

	isServerError: function(err) {
		return (err instanceof Http5XXError) ||
					 hasMarketoCode(err, mkCode.TIMED_OUT) ||
					 hasMarketoCode(err, mkCode.API_UNAVAILBLE) ||
					 hasMarketoCode(err, mkCode.SYSTEM_ERROR);
	},

	isExpiredToken: function(err) {
		return hasMarketoCode(err, mkCode.TOKEN_EXPIRED) ||
		 hasMarketoCode(err, mkCode.TOKEN_INVALID);
	},

	isRateLimited: function(err) {
		return hasMarketoCode(err, mkCode.RATE_LIMIT_REACHED);
	}
};
<commit_msg>Spaces<commit_after>var _ = require('lodash'),
  util = require('util'),
  retryableNetworkErrorCodes,

  // Marketo error codes
  // http://developers.marketo.com/documentation/rest/error-codes/
  mkCode = {
  TOKEN_EMPTY: '600',
  TOKEN_INVALID: '601',
  TOKEN_EXPIRED: '602',
  ACCESS_DENIED: '603',
  TIMED_OUT: '604',
  HTTP_NOT_SUPPORTED: '605',
  RATE_LIMIT_REACHED: '606',
  QUOTA_REACHED: '607',
  API_UNAVAILBLE: '608',
  INVALID_JSON: '609',
  RESOURCE_NOT_FOUND: '610',
  SYSTEM_ERROR: '611',
  INVALID_CONTENT_TYPE: '612',

  INVALID_VALUE: '1001',
  MISSING_REQUIRED_VALUE: '1002',
  INVALID_DATA: '1003',
  LEAD_NOT_FOUND: '1004',
  LEAD_ALREADY_EXISTS: '1005',
  FIELD_NOT_FOUND: '1006',
  MULTIPLE_MATCHING_LEADS: '1007',
  PARTITION_ACCESS_DENIED: '1008',
  PARTITION_UNSPECIFIED: '1009',
  PARTITION_UPDATE_NOT_ALLOWED: '1010',
  FIELD_UNSUPPORTED: '1011',
  INVALID_COOKIE: '1012',
  OBJECT_NOT_FOUND: '1013',
  OBJECT_CREATE_FAILED: '1014'
  };

// These error codes were pulled from
// https://github.com/joyent/node/blob/857975d5e7e0d7bf38577db0478d9e5ede79922e/deps/uv/include/uv-errno.h
// Is there a better way to do this?
retryableNetworkErrorCodes = [
  'EADDRINFO',
  'EADDRNOTAVAIL',
  'ECONNRESET',
  'ENOTFOUND',
  'ECONNABORTED',
  'EPROTO',
  'ECONNREFUSED',
  'EHOSTUNREACH',
  'ENETDOWN',
  'ENETUNREACH',
  'ENONET',
  'ENOTCONN',
  'ENOTSOCK',
  'ETIMEDOUT'
];

// for the record, here's the format of the Marketo data in case of error:
// {
//   requestId: '15381#161d91d807a',
//   success: false,
//   errors: [ { code: '605', message: 'Request method \'POST\' not supported' } ]
//  }

function getFirstMessage(data) {
  return data && data.errors ? data.errors[0] : 'Unknown Marketo error';
}

function Http5XXError(statusCode, data) {
  Error.call(this);
  this.message = getFirstMessage(data);
  this.data = data;
  this.code = statusCode;
}
util.inherits(Http5XXError, Error);

function Http4XXError(statusCode, data) {
  Error.call(this);
  this.message = getFirstMessage(data);
  this.data = data;
  this.code = statusCode;
}
util.inherits(Http4XXError, Error);

function TimeoutError(timeout) {
  Error.call(this, 'Timed out after ' + timeout + ' ms.');
}
util.inherits(TimeoutError, Error);

function hasMarketoCode(err, code) {
  return isMarketoError(err) &&
   !!_.find(err.errors, {code: code});
}

function isMarketoError(err) {
  var val = _.has(err, 'requestId') &&
  _.isArray(err.errors) &&
  err.errors.length > 0;
  return val;
}

module.exports = {
  Http5XXError: Http5XXError,
  Http4XXError: Http4XXError,
  isMarketoError: isMarketoError,
  hasMarketoCode: hasMarketoCode,
  marketoErrorCodes: mkCode,

  isNetworkError: function(err) {
  var code = err.code || '';
  return retryableNetworkErrorCodes.indexOf(code) > -1 ||
   (err instanceof TimeoutError);
  },

  isServerError: function(err) {
  return (err instanceof Http5XXError) ||
   hasMarketoCode(err, mkCode.TIMED_OUT) ||
   hasMarketoCode(err, mkCode.API_UNAVAILBLE) ||
   hasMarketoCode(err, mkCode.SYSTEM_ERROR);
  },

  isExpiredToken: function(err) {
  return hasMarketoCode(err, mkCode.TOKEN_EXPIRED) ||
   hasMarketoCode(err, mkCode.TOKEN_INVALID);
  },

  isRateLimited: function(err) {
  return hasMarketoCode(err, mkCode.RATE_LIMIT_REACHED);
  }
};
<|endoftext|>


<commit_before>var defaultMsg = {
  500: 'unknown error',
  401: 'unauthorized',
  404: 'not found'
}

module.exports.forCode = function forCode (code, msg) {
  code = code || 500
  var error = Error(msg || defaultMsg[code])
  error.statusCode = code
  return error
}
<commit_msg>slightly better default error messages<commit_after>var defaultMsg = {
  500: 'Unknown error',
  401: 'Unauthorized',
  404: 'Not found'
}

module.exports.forCode = function forCode (code, msg) {
  code = code || 500
  var error = Error(msg || defaultMsg[code])
  error.statusCode = code
  return error
}
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var auth = require('./authentications');

exports.register = function (server, options, next) {
  options = _.extend({ basePath: '/api/2.0' }, options);
  var handlers = require('./handlers.js')(server, 'cd-events');

  server.route([{
    method: 'POST',
    path: options.basePath + '/events/save',
    handler: handlers.actHandlerNeedsUser('saveEvent', ['eventInfo', 'eventId']),
    config: {
      auth: auth.apiUser,
      description: 'Save an event',
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/{id}',
    handler: handlers.actHandler('getEvent', 'id'),
    config: {
      description: 'Load event',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events',
    handler: handlers.actHandler('listEvents'),
    config: {
      description: 'List events',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/search',
    handler: handlers.actHandler('searchEvents'),
    config: {
      description: 'Search events',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/applications/{eventId}',
    handler: handlers.actHandlerNeedsUser('loadEventApplications', 'eventId'),
    config: {
      auth: auth.apiUser,
      description: 'Load event applications',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/applications/search',
    handler: handlers.actHandlerNeedsUser('searchApplications'),
    config: {
      auth: auth.apiUser,
      description: 'Search applications',
      tags: ['api', 'events']
    }
  }, {
    method: 'DELETE',
    path: options.basePath + '/events/{eventId}/applications/{applicationId}',
    handler: handlers.actHandlerNeedsUser('deleteApplication', ['eventId', 'applicationId']),
    config: {
      auth: auth.apiUser,
      description: 'Remove an application of an event',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/application',
    handler: handlers.actHandlerNeedsUser('saveApplication'),
    config: {
      auth: auth.apiUser,
      description: 'Save an application',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/user-dojos-events',
    handler: handlers.actHandlerNeedsUser('userDojosEvents'),
    config: {
      auth: auth.apiUser,
      description: 'User\'s events',
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/tickets/types',
    handler: handlers.actHandler('ticketTypes'),
    config: {
      description: 'List ticket types',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/export-guest-list/dojo/{dojoId}/event/{eventId}/{status}-export.csv',
    handler: handlers.actHandlerNeedsUser('exportGuestList', ['dojoId', 'eventId', 'status'], {type: 'csv'}),
    config: {
      auth: auth.apiUser,
      description: 'Export user list by event and status',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/sessions/search',
    handler: handlers.actHandler('searchSessions'),
    config: {
      description: 'Search a session',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/bulk-apply-applications',
    handler: handlers.actHandlerNeedsUser('bulkApplyApplications'),
    config: {
      auth: auth.apiUser,
      description: 'Update applications by bulk',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/update-application-attendance',
    handler: handlers.actHandlerNeedsUser('updateApplicationAttendance'),
    config: {
      auth: auth.apiUser,
      description: 'Update attendance of an application',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/session/cancel',
    handler: handlers.actHandlerNeedsUser('cancelSession'),
    config: {
      auth: auth.apiUser,
      description: 'Cancel an event',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/session/validate-invitation',
    handler: handlers.actHandlerNeedsUser('validateSessionInvitation'),
    config: {
      auth: auth.apiUser,
      description: 'Validate an invitation',
      tags: ['api', 'events']
    }
  }]);

  next();
};

exports.register.attributes = {
  name: 'api-events'
};
<commit_msg>Added REST API to get sessions for a given event ID (#1142)<commit_after>'use strict';

var _ = require('lodash');
var auth = require('./authentications');
var Joi = require('joi');

exports.register = function (server, options, next) {
  options = _.extend({ basePath: '/api/2.0' }, options);
  var handlers = require('./handlers.js')(server, 'cd-events');

  server.route([{
    method: 'POST',
    path: options.basePath + '/events/save',
    handler: handlers.actHandlerNeedsUser('saveEvent', ['eventInfo', 'eventId']),
    config: {
      auth: auth.apiUser,
      description: 'Save an event',
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/{id}',
    handler: handlers.actHandler('getEvent', 'id'),
    config: {
      description: 'Load event',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/{id}/sessions',
    handler: handlers.actHandler('getSessionsFromEventId', 'id'),
    config: {
      description: 'Load sessions for an event',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events'],
      validate: {
        params: {
          id: Joi.string().guid()
        }
      }
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events',
    handler: handlers.actHandler('listEvents'),
    config: {
      description: 'List events',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/search',
    handler: handlers.actHandler('searchEvents'),
    config: {
      description: 'Search events',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/applications/{eventId}',
    handler: handlers.actHandlerNeedsUser('loadEventApplications', 'eventId'),
    config: {
      auth: auth.apiUser,
      description: 'Load event applications',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/applications/search',
    handler: handlers.actHandlerNeedsUser('searchApplications'),
    config: {
      auth: auth.apiUser,
      description: 'Search applications',
      tags: ['api', 'events']
    }
  }, {
    method: 'DELETE',
    path: options.basePath + '/events/{eventId}/applications/{applicationId}',
    handler: handlers.actHandlerNeedsUser('deleteApplication', ['eventId', 'applicationId']),
    config: {
      auth: auth.apiUser,
      description: 'Remove an application of an event',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/application',
    handler: handlers.actHandlerNeedsUser('saveApplication'),
    config: {
      auth: auth.apiUser,
      description: 'Save an application',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/user-dojos-events',
    handler: handlers.actHandlerNeedsUser('userDojosEvents'),
    config: {
      auth: auth.apiUser,
      description: 'User\'s events',
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/tickets/types',
    handler: handlers.actHandler('ticketTypes'),
    config: {
      description: 'List ticket types',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'GET',
    path: options.basePath + '/events/export-guest-list/dojo/{dojoId}/event/{eventId}/{status}-export.csv',
    handler: handlers.actHandlerNeedsUser('exportGuestList', ['dojoId', 'eventId', 'status'], {type: 'csv'}),
    config: {
      auth: auth.apiUser,
      description: 'Export user list by event and status',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/sessions/search',
    handler: handlers.actHandler('searchSessions'),
    config: {
      description: 'Search a session',
      cors: { origin: ['*'], credentials: false },
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/bulk-apply-applications',
    handler: handlers.actHandlerNeedsUser('bulkApplyApplications'),
    config: {
      auth: auth.apiUser,
      description: 'Update applications by bulk',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/update-application-attendance',
    handler: handlers.actHandlerNeedsUser('updateApplicationAttendance'),
    config: {
      auth: auth.apiUser,
      description: 'Update attendance of an application',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/session/cancel',
    handler: handlers.actHandlerNeedsUser('cancelSession'),
    config: {
      auth: auth.apiUser,
      description: 'Cancel an event',
      tags: ['api', 'events']
    }
  }, {
    method: 'POST',
    path: options.basePath + '/events/session/validate-invitation',
    handler: handlers.actHandlerNeedsUser('validateSessionInvitation'),
    config: {
      auth: auth.apiUser,
      description: 'Validate an invitation',
      tags: ['api', 'events']
    }
  }]);

  next();
};

exports.register.attributes = {
  name: 'api-events'
};
<|endoftext|>


<commit_before>/**
 * Setup the user (DOM) event flow.
 *
 * @private
 * @param {object} The moule instnace.
*/
function setupDomEventFlow (scope, data) {

    var self = this;

    if (!config || !config.domEvents) {
        return;
    }

    var events = config.domEvents;

    for (var i = 0, l = events.length, event, stream; i < l; ++i) {
        event = events[i];

        // handle element config
        if (event.element) {
            event.selector = "[data-element='" + event.element + "']";
            delete event.element;
        }

        var elms = scope.querySelectorAll(event.selector);
        if (elms) {
            // create event stream
            stream = self.flow(event.flow);
            stream.context = {
                renderData: data,
                dontPrevent: event.dontPrevent
            };

            for (var e = 0; e < elms.length; ++e) {
                elms[e].addEventListener(event.on, domEventListenerClosure(stream, elms, data));
            }
        }
    }
}

function domEventListenerClosure (stream, elms, data) {
    return function (event) {
        // dont prevent default browser actions
        if (!stream.context.dontPrevent) {
            event.preventDefault();
        }

        stream.write(null, {
            event: event,
            elms: elms,
            item: data
        });
    };
}
<commit_msg>refactor event listener setup<commit_after>/**
 * Setup the user (DOM) event flow.
 *
 * @private
 * @param {object} The moule instnace.
*/
module.exports = function (instance, template, options, dom, data) {

    template.events.forEach(function (listener) {

        // handle element config
        if (listener.element) {
            listener.selector = "[data-element='" + listener.element + "']";
            delete listener.element;
        }

        var elms = dom.querySelectorAll(listener.selector || listener.sel);
        if (elms) {

            // create event stream
            if (!template.streams[listener.flow]) {
                template.streams[listener.flow] = instance.flow(listener.flow);
            }

            Array.from(elms).forEach(function (elm) {
                elm.addEventListener(listener.on, function (event) {

                    // dont prevent default browser actions
                    if (!options.dontPrevent) {
                        event.preventDefault();
                    }

                    event.data = data;
                    event.template = template.to;
                    template.streams[listener.flow].write(event);
                });
            });
        }
    });
}
<|endoftext|>


<commit_before>var _ = require('lodash');
var moment = require('moment');
var Promise = require('bluebird');
var debug = require('debug')('lib:events');
var os = require('os');
var nconf = require('nconf');

var signer = require('nacl-signature');
var bs58 = require('bs58');

var mongo = require('./mongo');
var utils = require('./utils');
var alarms = require('./alarms');

function hasError(retDict) {
    return (!_.isUndefined(_.get(retDict, 'error')));
};

function reportError(where, err) {
    debug("%s Error detected and raised %s: %s",
        req.randomUnicode, where, err);
    return alarms.reportAlarm({
        caller: 'events',
        what: where,
        info: err
    })
    .then(function() {
        throw new Error(where + '-' + err);
    });
};

function processHeaders(received, required) {
    var ret = {};
    var errs = _.map(required, function(destkey, headerName) {
        var r = _.get(received, headerName);
        if(_.isUndefined(r))
            return headerName;

        _.set(ret, destkey, r);
        return null;
    });
    errs = _.compact(errs);
    if(_.size(errs)) {
        debug("Error in processHeaders: %j", errs);
        return { 'errors': errs };
    }
    return ret;
};

function parseEvents(memo, evnt) {

    if(evnt.type === 'timeline') {
        newTmln = {};
        newTmln.startTime = new Date(moment(evnt.startTime).toISOString());
        newTmln.userId = memo.sessionInfo.numId;
        newTmln.geoip = memo.sessionInfo.geoip;
        newTmln.id = utils.hash({
            'uuid': evnt.id,
            'user': memo.sessionInfo.numId
        });

        if(evnt.location !== 'https://www.facebook.com/')
            newTmln.nonfeed = true;

        if(_.get(evnt, 'tagId'))
            newTmln.tagId = evnt.tagId;

        memo.timelines.push(newTmln);
        return memo;
    }

    if(evnt.type === 'impression') {
        var impression = _.pick(evnt, ['visibility', 'html' ] );

        impression.timelineId = utils.hash({
            'uuid': evnt.timelineId,
            'user': memo.sessionInfo.numId
        });
        impression.id = utils.hash({
            'uuid': evnt.timelineId,
            'user': memo.sessionInfo.numId,
            'order': evnt.impressionOrder
        });
        impression.userId = memo.sessionInfo.numId;

        impression.impressionOrder = _.parseInt(evnt.impressionOrder);
        impression.impressionTime = new Date(
            moment(evnt.impressionTime).toISOString()
        );

        if(_.eq(impression.visibility, 'public')) {

            if(!_.size(impression.html))
                debug("Strange: public impression with zero size HTML?");

            impression.htmlId = utils.hash({ 'html': impression.html });

            var snippet = {
                savingTime: new Date(moment().toISOString()),
                id: impression.htmlId,
                userId: memo.sessionInfo.numId,
                impressionId: impression.id,
                timelineId: impression.timelineId,
                html: impression.html
            };
            memo.htmls.push(snippet);
        } else if( _.size(impression.html) ) {
            debug("Warning! private post leakage? %d",
                _.size(impression.html));
            /* this is impossible, but if happen I want to see it */
        }

        memo.impressions.push(_.omit(impression, ['html']));
        return memo;
    }

    debug("Unexpected type: %s, abort", evnt.type);

    if (typeof memo.error === 'undefined') {
        memo.error = [];
    }

    memo.error.push(JSON.stringify({
        'kind': "invalid type",
        'event': evnt
    }));
    return memo;
};

function promisifyInputs(body, geoinfo, supporter) {

    var processed = _.reduce(body, parseEvents, {
        'timelines': [],
        'impressions': [],
        'htmls': [],
        'errors': [],
        'sessionInfo': {
            'geoip': geoinfo,
            'numId': supporter.userId,
            'publicKey': supporter.publicKey,
            'version': supporter.version
        }
    });

    if(hasError(processed))
        reportError('body parsing', processed.error);

    var functionList = [];

    if(_.size(processed.htmls))
        functionList.push(mongo
            .writeMany(nconf.get('schema').htmls, processed.htmls)
            .return({
                'kind': 'htmls',
                'amount': _.size(processed.htmls)
            })
        );

    if(_.size(processed.impressions))
        functionList.push(mongo
            .writeMany(nconf.get('schema').impressions,processed.impressions)
            .return({
                'kind': 'impressions',
                'amount': _.size(processed.impressions)
            })
        );

    if(_.size(processed.timelines))
        functionList.push( mongo
            .writeMany(nconf.get('schema').timelines, processed.timelines)
            .return({
                'kind': 'timelines',
                'amount': _.size(processed.timelines)
            })
        );

    functionList.push(
        mongo
            .updateOne(nconf.get('schema').supporters, {
                    publicKey: supporter.publicKey,
                    userId: _.parseInt(supporter.userId)
                },
                _.set(supporter, 'lastActivity',
                    new Date(moment().toISOString()))
             )
    );

    debug(" * %d timelines %d impressions %d html %s",
        _.size(processed.timelines),
        _.size(processed.impressions), _.size(processed.htmls),
        _.get(processed.timelines[0], 'tagId') ?
            "tagId " + processed.timelines[0].tagId :
            ""
        );

    debug(" * user %d [%s] last activity %s (%s ago) %s",
        supporter.userId, geoinfo,
        moment(supporter.lastActivity).format("HH:mm DD/MM"),
        moment.duration(moment.utc()-moment(supporter.lastActivity)).humanize(),
        supporter.version);

    return functionList;
};

function processEvents(req) {

    var ipaddr = _.get(req.headers, "x-forwarded-for") || "127.0.0.1";
    var geoip = utils.getGeoIP(ipaddr);
    var geoinfo = geoip.code;

    if(!geoinfo) {
        geoinfo = geoip.ip;
        if(geoinfo !== "127.0.0.1") {
            debug("Anomaly, unresolved IP different from localhost: %s", geoinfo);
            geoinfo = "redacted";
        }
    }

    var headers = processHeaders(_.get(req, 'headers'), {
        'content-length': 'length',
        'x-fbtrex-build': 'build',
        'x-fbtrex-version': 'version',
        'x-fbtrex-userid': 'supporterId',
        'x-fbtrex-publickey': 'publickey',
        'x-fbtrex-signature': 'signature'
    });
    if(hasError(headers))
        reportError('header parsing, missing', headers.error);

    return mongo
        .read(nconf.get('schema').supporters, {
            userId: _.parseInt(headers.supporterId),
            publicKey: headers.publickey
        })
        // TODO: we can remove the next `.tap` by delegating the uniqueness
        // check to MongoDB. We can achieve this by creating a compound key
        // db.supporters.createIndex( { "userId": 1, "publicKey": 1 } )
        .tap(function(supporterL) {
            if(_.size(supporterL) > 1) {
                debug("Impossible? dup supporter %j", supporterL);
                return alarms.reportAlarm({
                    caller: 'events',
                    what: 'duplicated user',
                    info: supporterL
                });
            }
            if(!_.size(supporterL)) {
                debug("Bad: missing supporter %j", headers);
                return alarms.reportAlarm({
                    caller: 'events',
                    what: 'supporter not found',
                    info: headers
                })
                .then(function() {
                    throw new Error("User not found");
                });
            }
        })
        .then(_.first)
        .then(function(supporter) {
            if(!_.every([ headers.signature, supporter.publicKey], _.size)) {
                debug("Validation fail: signed %s retrieved pubkey %s",
                    headers.signature, supporter.publicKey);
                throw new Error("Signature can't be verify");
            }
            if (!utils.verifyRequestSignature(req)) {
                throw new Error('Signature does not match request body');
            }
            /* verification went well! */
            supporter.version = headers.version;
            return supporter;
        })
        .then(function(supporter) {
            return promisifyInputs(req.body, geoinfo, supporter);
        })
        .all()
        .then(function(results) {
            return { "json": {
                "status": "OK",
                "info": results
            }};
        })
        .catch(function(error) {
            debug("Event submission ignored: %s", error.message);
            return { 'json': {
                'status': 'error',
                'info': error.message
            }};
        });
};

module.exports = {
  processEvents: processEvents
};
<commit_msg>simple 1 line debug removal<commit_after>var _ = require('lodash');
var moment = require('moment');
var Promise = require('bluebird');
var debug = require('debug')('lib:events');
var os = require('os');
var nconf = require('nconf');

var signer = require('nacl-signature');
var bs58 = require('bs58');

var mongo = require('./mongo');
var utils = require('./utils');
var alarms = require('./alarms');

function hasError(retDict) {
    return (!_.isUndefined(_.get(retDict, 'error')));
};

function reportError(where, err) {
    debug("%s Error detected and raised %s: %s",
        req.randomUnicode, where, err);
    return alarms.reportAlarm({
        caller: 'events',
        what: where,
        info: err
    })
    .then(function() {
        throw new Error(where + '-' + err);
    });
};

function processHeaders(received, required) {
    var ret = {};
    var errs = _.map(required, function(destkey, headerName) {
        var r = _.get(received, headerName);
        if(_.isUndefined(r))
            return headerName;

        _.set(ret, destkey, r);
        return null;
    });
    errs = _.compact(errs);
    if(_.size(errs)) {
        debug("Error in processHeaders: %j", errs);
        return { 'errors': errs };
    }
    return ret;
};

function parseEvents(memo, evnt) {

    if(evnt.type === 'timeline') {
        newTmln = {};
        newTmln.startTime = new Date(moment(evnt.startTime).toISOString());
        newTmln.userId = memo.sessionInfo.numId;
        newTmln.geoip = memo.sessionInfo.geoip;
        newTmln.id = utils.hash({
            'uuid': evnt.id,
            'user': memo.sessionInfo.numId
        });

        if(evnt.location !== 'https://www.facebook.com/')
            newTmln.nonfeed = true;

        if(_.get(evnt, 'tagId'))
            newTmln.tagId = evnt.tagId;

        memo.timelines.push(newTmln);
        return memo;
    }

    if(evnt.type === 'impression') {
        var impression = _.pick(evnt, ['visibility', 'html' ] );

        impression.timelineId = utils.hash({
            'uuid': evnt.timelineId,
            'user': memo.sessionInfo.numId
        });
        impression.id = utils.hash({
            'uuid': evnt.timelineId,
            'user': memo.sessionInfo.numId,
            'order': evnt.impressionOrder
        });
        impression.userId = memo.sessionInfo.numId;

        impression.impressionOrder = _.parseInt(evnt.impressionOrder);
        impression.impressionTime = new Date(
            moment(evnt.impressionTime).toISOString()
        );

        if(_.eq(impression.visibility, 'public')) {

            if(!_.size(impression.html))
                debug("Strange: public impression with zero size HTML?");

            impression.htmlId = utils.hash({ 'html': impression.html });

            var snippet = {
                savingTime: new Date(moment().toISOString()),
                id: impression.htmlId,
                userId: memo.sessionInfo.numId,
                impressionId: impression.id,
                timelineId: impression.timelineId,
                html: impression.html
            };
            memo.htmls.push(snippet);
        } else if( _.size(impression.html) ) {
            debug("Warning! private post leakage? %d",
                _.size(impression.html));
            /* this is impossible, but if happen I want to see it */
        }

        memo.impressions.push(_.omit(impression, ['html']));
        return memo;
    }

    debug("Unexpected type: %s, abort", evnt.type);

    if (typeof memo.error === 'undefined') {
        memo.error = [];
    }

    memo.error.push(JSON.stringify({
        'kind': "invalid type",
        'event': evnt
    }));
    return memo;
};

function promisifyInputs(body, geoinfo, supporter) {

    var processed = _.reduce(body, parseEvents, {
        'timelines': [],
        'impressions': [],
        'htmls': [],
        'errors': [],
        'sessionInfo': {
            'geoip': geoinfo,
            'numId': supporter.userId,
            'publicKey': supporter.publicKey,
            'version': supporter.version
        }
    });

    if(hasError(processed))
        reportError('body parsing', processed.error);

    var functionList = [];

    if(_.size(processed.htmls))
        functionList.push(mongo
            .writeMany(nconf.get('schema').htmls, processed.htmls)
            .return({
                'kind': 'htmls',
                'amount': _.size(processed.htmls)
            })
        );

    if(_.size(processed.impressions))
        functionList.push(mongo
            .writeMany(nconf.get('schema').impressions,processed.impressions)
            .return({
                'kind': 'impressions',
                'amount': _.size(processed.impressions)
            })
        );

    if(_.size(processed.timelines))
        functionList.push( mongo
            .writeMany(nconf.get('schema').timelines, processed.timelines)
            .return({
                'kind': 'timelines',
                'amount': _.size(processed.timelines)
            })
        );

    functionList.push(
        mongo
            .updateOne(nconf.get('schema').supporters, {
                    publicKey: supporter.publicKey,
                    userId: _.parseInt(supporter.userId)
                },
                _.set(supporter, 'lastActivity',
                    new Date(moment().toISOString()))
             )
    );

    debug(" * %d timelines %d impressions %d html %s",
        _.size(processed.timelines),
        _.size(processed.impressions), _.size(processed.htmls),
        _.get(processed.timelines[0], 'tagId') ?
            "tagId " + processed.timelines[0].tagId :
            ""
        );

    debug(" * user %d [%s] last activity %s (%s ago) %s",
        supporter.userId, geoinfo,
        moment(supporter.lastActivity).format("HH:mm DD/MM"),
        moment.duration(moment.utc()-moment(supporter.lastActivity)).humanize(),
        supporter.version);

    return functionList;
};

function processEvents(req) {

    var ipaddr = _.get(req.headers, "x-forwarded-for") || "127.0.0.1";
    var geoip = utils.getGeoIP(ipaddr);
    var geoinfo = geoip.code;

    if(!geoinfo) {
        geoinfo = geoip.ip;
        if(geoinfo !== "127.0.0.1") {
            // debug("Anomaly, unresolved IP different from localhost: %s", geoinfo);
            geoinfo = "redacted";
        }
    }

    var headers = processHeaders(_.get(req, 'headers'), {
        'content-length': 'length',
        'x-fbtrex-build': 'build',
        'x-fbtrex-version': 'version',
        'x-fbtrex-userid': 'supporterId',
        'x-fbtrex-publickey': 'publickey',
        'x-fbtrex-signature': 'signature'
    });
    if(hasError(headers))
        reportError('header parsing, missing', headers.error);

    return mongo
        .read(nconf.get('schema').supporters, {
            userId: _.parseInt(headers.supporterId),
            publicKey: headers.publickey
        })
        // TODO: we can remove the next `.tap` by delegating the uniqueness
        // check to MongoDB. We can achieve this by creating a compound key
        // db.supporters.createIndex( { "userId": 1, "publicKey": 1 } )
        .tap(function(supporterL) {
            if(_.size(supporterL) > 1) {
                debug("Impossible? dup supporter %j", supporterL);
                return alarms.reportAlarm({
                    caller: 'events',
                    what: 'duplicated user',
                    info: supporterL
                });
            }
            if(!_.size(supporterL)) {
                debug("Bad: missing supporter %j", headers);
                return alarms.reportAlarm({
                    caller: 'events',
                    what: 'supporter not found',
                    info: headers
                })
                .then(function() {
                    throw new Error("User not found");
                });
            }
        })
        .then(_.first)
        .then(function(supporter) {
            if(!_.every([ headers.signature, supporter.publicKey], _.size)) {
                debug("Validation fail: signed %s retrieved pubkey %s",
                    headers.signature, supporter.publicKey);
                throw new Error("Signature can't be verify");
            }
            if (!utils.verifyRequestSignature(req)) {
                throw new Error('Signature does not match request body');
            }
            /* verification went well! */
            supporter.version = headers.version;
            return supporter;
        })
        .then(function(supporter) {
            return promisifyInputs(req.body, geoinfo, supporter);
        })
        .all()
        .then(function(results) {
            return { "json": {
                "status": "OK",
                "info": results
            }};
        })
        .catch(function(error) {
            debug("Event submission ignored: %s", error.message);
            return { 'json': {
                'status': 'error',
                'info': error.message
            }};
        });
};

module.exports = {
  processEvents: processEvents
};
<|endoftext|>


<commit_before>Rye.define('Events', function(){

    var util = Rye.require('Util')
      , query = Rye.require('Query')
      , _slice = Array.prototype.slice

    // General-purpose event emitter
    // -----------------------------

    function EventEmitter () {
        this.events = {}
        this.context = null
    }

    // Adds a handler to the events list
    EventEmitter.prototype.addListener = function (event, handler) {
        var handlers = this.events[event] || (this.events[event] = [])
        handlers.push(handler)
        return this
    }

    // Add a handler that can only get called once
    EventEmitter.prototype.once = function (event, handler) {
        var self = this
        function suicide () {
            handler.apply(this, arguments)
            self.removeListener(event, suicide)
        }
        return this.addListener(event, suicide)
    }

    // Removes a handler from the events list
    EventEmitter.prototype.removeListener = function (event, handler) {
        var self = this
          , handlers = this.events[event]
        if (event === '*') {
            if (!handler) {
                this.events = {}
            } else {
                Object.keys(this.events).forEach(function(event){
                    self.removeListener(event, handler)
                })
            }
        } else if (handler && handlers) {
            handlers.splice(handlers.indexOf(handler), 1)
            if (handlers.length === 0) {
                delete this.events[event]
            }
        } else {
            delete this.events[event]
        }
        return this
    }

    // Calls all handlers that match the event type
    EventEmitter.prototype.emit = function (event) {
        var handlers = this.events[event]
          , args = _slice.call(arguments, 1)
          , fn, i

        if (handlers) {
            for (i = 0; fn = handlers[i++]; ){
                fn.apply(this.context || this, args)
            }
        }
        return this
    }

    EventEmitter.prototype.proxy = function (event) {
        return util.applier('left', this.emit, this, [event])
    }

    // Utility methods
    // -----------------------------

    var emitters = {}

    function getEmitter (element) {
        var id = util.getUid(element)
        return emitters[id] || (emitters[id] = new DOMEventEmitter(element))
    }

    function parseEvent (name) {
        var index = name.indexOf(' ')
        return {
            event    : index > 0 ? name.substr(0, index) : name
          , selector : index > 0 ? name.substr(index) : ''
        }
    }

    function createEvent (type, properties) {
        if (typeof type != 'string') {
            type = type.type
        }
        var isMouse = ['click', 'mousedown', 'mouseup', 'mousemove'].indexOf(type) != -1
          , event = document.createEvent(isMouse ? 'MouseEvent' : 'Event')
        if (properties) {
            util.extend(event, properties)
        }
        event.initEvent(type, true, true)
        return event
    }

    // DOM event emitter
    // -----------------------------

    /*
        Creates one event emitter per element, proxies DOM events to it. This way
        we can keep track of the functions so that they can be removed from the
        elements by reference when you call .removeListener() by event name.
    */

    function DOMEventEmitter (element) {
        EventEmitter.call(this)
        this.element = element
        this.proxied = {}
    }

    util.inherits(DOMEventEmitter, EventEmitter)

    DOMEventEmitter.prototype._proxy = function (name) {
        return function (event) {
            var selector = parseEvent(name).selector
              , context = this.element
            // delegate behavior
            if (selector) {
                context = event.target
                while (context && !query.matches(context, selector)) {
                    context = context !== this.element && context.parentNode
                }
                if (!context || context == this.element) {
                    return
                }
            }
            this.context = context
            this.emit(name, event, this.element)
        }.bind(this)
    }

    DOMEventEmitter.prototype.proxy = function (name) {
        return this.proxied[name] || (this.proxied[name] = this._proxy(name))
    }

    DOMEventEmitter.prototype.addListener = function (name, handler) {
        EventEmitter.prototype.addListener.call(this, name, handler)
        if (!this.proxied[name]) {
            this.element.addEventListener(parseEvent(name).event, this.proxy(name), false)
        }
        return this
    }

    DOMEventEmitter.prototype.removeListener = function (name, handler) {
        if (name.indexOf('*') >= 0) {
            var self = this
              , re = new RegExp('^' + name.replace('*', '\\b'))
            // *      : remove all events
            // type * : remove delegate events
            // type*  : remove delegate and undelegate
            Object.keys(this.events).forEach(function(name){
                if (re.test(name)) {
                    self.removeListener(name, handler)
                }
            })
        } else {
            var proxy = this.proxied[name]
            EventEmitter.prototype.removeListener.call(this, name, handler)
            if (!this.events[name] && proxy) {
                this.element.removeEventListener(parseEvent(name).event, proxy, false)
                delete this.proxied[name]
            }
        }
        return this
    }

    function acceptMultipleEvents (method) {
        var _method = DOMEventEmitter.prototype[method]
        DOMEventEmitter.prototype[method] = function (name, handler) {
            var self = this
            if (typeof name !== 'string') {
                Object.keys(name).forEach(function(key){
                    _method.call(self, key, name[key])
                })
            } else {
                _method.call(self, name, handler)
            }
            return self
        }
    }

    ;['addListener', 'once', 'removeListener'].forEach(acceptMultipleEvents)

    DOMEventEmitter.prototype.destroy = function () {
        return this.removeListener('*')
    }

    DOMEventEmitter.prototype.trigger = function (event, data) {
        if (!(event instanceof window.Event)) {
            event = createEvent(event)
        }
        event.data = data
        this.element.dispatchEvent(event)
        return this
    }

    // Exported methods
    // -----------------------------

    var exports = {}

    function emitterProxy (method, element, event, handler) {
        getEmitter(element)[method](event, handler)
    }

    ;['addListener', 'removeListener', 'once', 'trigger'].forEach(function(method){
        // Create a function proxy for the method
        var fn = util.curry(emitterProxy, method)
        // Exports module and rye methods
        exports[method] = fn
        this[method] = this.iterate(fn)
    }.bind(this))

    // Aliases
    // -----------------------------

    ;[EventEmitter.prototype, DOMEventEmitter.prototype, exports, this].forEach(function(obj){
        obj.on = obj.addListener
    })

    // Global event bus / pub-sub
    // -----------------------------

    var EE = new EventEmitter

    Rye.subscribe   = EE.addListener.bind(EE)
    Rye.unsubscribe = EE.removeListener.bind(EE)
    Rye.publish     = EE.emit.bind(EE)

    return {
        EventEmitter    : EventEmitter
      , DOMEventEmitter : DOMEventEmitter
      , getEmitter      : getEmitter
      , createEvent     : createEvent
      , addListener     : exports.addListener
      , on              : exports.on
      , once            : exports.once
      , removeListener  : exports.removeListener
      , trigger         : exports.trigger
    }
})
<commit_msg>Remove exportation of `on` in events<commit_after>Rye.define('Events', function(){

    var util = Rye.require('Util')
      , query = Rye.require('Query')
      , _slice = Array.prototype.slice

    // General-purpose event emitter
    // -----------------------------

    function EventEmitter () {
        this.events = {}
        this.context = null
    }

    // Adds a handler to the events list
    EventEmitter.prototype.addListener = function (event, handler) {
        var handlers = this.events[event] || (this.events[event] = [])
        handlers.push(handler)
        return this
    }

    // Add a handler that can only get called once
    EventEmitter.prototype.once = function (event, handler) {
        var self = this
        function suicide () {
            handler.apply(this, arguments)
            self.removeListener(event, suicide)
        }
        return this.addListener(event, suicide)
    }

    // Removes a handler from the events list
    EventEmitter.prototype.removeListener = function (event, handler) {
        var self = this
          , handlers = this.events[event]
        if (event === '*') {
            if (!handler) {
                this.events = {}
            } else {
                Object.keys(this.events).forEach(function(event){
                    self.removeListener(event, handler)
                })
            }
        } else if (handler && handlers) {
            handlers.splice(handlers.indexOf(handler), 1)
            if (handlers.length === 0) {
                delete this.events[event]
            }
        } else {
            delete this.events[event]
        }
        return this
    }

    // Calls all handlers that match the event type
    EventEmitter.prototype.emit = function (event) {
        var handlers = this.events[event]
          , args = _slice.call(arguments, 1)
          , fn, i

        if (handlers) {
            for (i = 0; fn = handlers[i++]; ){
                fn.apply(this.context || this, args)
            }
        }
        return this
    }

    EventEmitter.prototype.proxy = function (event) {
        return util.applier('left', this.emit, this, [event])
    }

    // Utility methods
    // -----------------------------

    var emitters = {}

    function getEmitter (element) {
        var id = util.getUid(element)
        return emitters[id] || (emitters[id] = new DOMEventEmitter(element))
    }

    function parseEvent (name) {
        var index = name.indexOf(' ')
        return {
            event    : index > 0 ? name.substr(0, index) : name
          , selector : index > 0 ? name.substr(index) : ''
        }
    }

    function createEvent (type, properties) {
        if (typeof type != 'string') {
            type = type.type
        }
        var isMouse = ['click', 'mousedown', 'mouseup', 'mousemove'].indexOf(type) != -1
          , event = document.createEvent(isMouse ? 'MouseEvent' : 'Event')
        if (properties) {
            util.extend(event, properties)
        }
        event.initEvent(type, true, true)
        return event
    }

    // DOM event emitter
    // -----------------------------

    /*
        Creates one event emitter per element, proxies DOM events to it. This way
        we can keep track of the functions so that they can be removed from the
        elements by reference when you call .removeListener() by event name.
    */

    function DOMEventEmitter (element) {
        EventEmitter.call(this)
        this.element = element
        this.proxied = {}
    }

    util.inherits(DOMEventEmitter, EventEmitter)

    DOMEventEmitter.prototype._proxy = function (name) {
        return function (event) {
            var selector = parseEvent(name).selector
              , context = this.element
            // delegate behavior
            if (selector) {
                context = event.target
                while (context && !query.matches(context, selector)) {
                    context = context !== this.element && context.parentNode
                }
                if (!context || context == this.element) {
                    return
                }
            }
            this.context = context
            this.emit(name, event, this.element)
        }.bind(this)
    }

    DOMEventEmitter.prototype.proxy = function (name) {
        return this.proxied[name] || (this.proxied[name] = this._proxy(name))
    }

    DOMEventEmitter.prototype.addListener = function (name, handler) {
        EventEmitter.prototype.addListener.call(this, name, handler)
        if (!this.proxied[name]) {
            this.element.addEventListener(parseEvent(name).event, this.proxy(name), false)
        }
        return this
    }

    DOMEventEmitter.prototype.removeListener = function (name, handler) {
        if (name.indexOf('*') >= 0) {
            var self = this
              , re = new RegExp('^' + name.replace('*', '\\b'))
            // *      : remove all events
            // type * : remove delegate events
            // type*  : remove delegate and undelegate
            Object.keys(this.events).forEach(function(name){
                if (re.test(name)) {
                    self.removeListener(name, handler)
                }
            })
        } else {
            var proxy = this.proxied[name]
            EventEmitter.prototype.removeListener.call(this, name, handler)
            if (!this.events[name] && proxy) {
                this.element.removeEventListener(parseEvent(name).event, proxy, false)
                delete this.proxied[name]
            }
        }
        return this
    }

    function acceptMultipleEvents (method) {
        var _method = DOMEventEmitter.prototype[method]
        DOMEventEmitter.prototype[method] = function (name, handler) {
            var self = this
            if (typeof name !== 'string') {
                Object.keys(name).forEach(function(key){
                    _method.call(self, key, name[key])
                })
            } else {
                _method.call(self, name, handler)
            }
            return self
        }
    }

    ;['addListener', 'once', 'removeListener'].forEach(acceptMultipleEvents)

    DOMEventEmitter.prototype.destroy = function () {
        return this.removeListener('*')
    }

    DOMEventEmitter.prototype.trigger = function (event, data) {
        if (!(event instanceof window.Event)) {
            event = createEvent(event)
        }
        event.data = data
        this.element.dispatchEvent(event)
        return this
    }

    // Exported methods
    // -----------------------------

    var exports = {}

    function emitterProxy (method, element, event, handler) {
        getEmitter(element)[method](event, handler)
    }

    ;['addListener', 'removeListener', 'once', 'trigger'].forEach(function(method){
        // Create a function proxy for the method
        var fn = util.curry(emitterProxy, method)
        // Exports module and rye methods
        exports[method] = fn
        this[method] = this.iterate(fn)
    }.bind(this))

    // Aliases
    // -----------------------------

    ;[EventEmitter.prototype, DOMEventEmitter.prototype, this].forEach(function(obj){
        obj.on = obj.addListener
    })

    // Global event bus / pub-sub
    // -----------------------------

    var EE = new EventEmitter

    Rye.subscribe   = EE.addListener.bind(EE)
    Rye.unsubscribe = EE.removeListener.bind(EE)
    Rye.publish     = EE.emit.bind(EE)

    return {
        EventEmitter    : EventEmitter
      , DOMEventEmitter : DOMEventEmitter
      , getEmitter      : getEmitter
      , createEvent     : createEvent
      , addListener     : exports.addListener
      , once            : exports.once
      , removeListener  : exports.removeListener
      , trigger         : exports.trigger
    }
})
<|endoftext|>