text: import vm from 'vm';
  
  import { Meteor } from 'meteor/meteor';
  import { HTTP } from 'meteor/http';
  import { Random } from 'meteor/random';
  import { Livechat } from 'meteor/rocketchat:livechat';
  import Fiber from 'fibers';
  import Future from 'fibers/future';
  import _ from 'underscore';
  import s from 'underscore.string';
  import moment from 'moment';
  
  import { logger } from '../logger';
  import { processWebhookMessage } from '../../../lib';
  import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server';
  import * as Models from '../../../models';
  import { settings } from '../../../settings/server';
  
  const compiledScripts = {};
  function buildSandbox(store = {}) {
  	const sandbox = {
  		scriptTimeout(reject) {
  			return setTimeout(() => reject('timed out'), 3000);
  		},
  		_,
  		s,
  		console,
  		moment,
  		Fiber,
  		Promise,
  		Livechat,
  		Store: {
  			set(key, val) {
  				store[key] = val;
  				return val;
  			},
  			get(key) {
  				return store[key];
  			},
  		},
  		HTTP(method, url, options) {
  			try {
  				return {
  					result: HTTP.call(method, url, options),
  				};
  			} catch (error) {
  				return {
  					error,
  				};
  			}
  		},
  	};
  	Object.keys(Models).filter((k) => !k.startsWith('_')).forEach((k) => { sandbox[k] = Models[k]; });
  	return { store, sandbox	};
  }
  
  function getIntegrationScript(integration) {
  	const compiledScript = compiledScripts[integration._id];
  	if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) {
  		return compiledScript.script;
  	}
  
  	const script = integration.scriptCompiled;
  	const { sandbox, store } = buildSandbox();
  	try {
  		logger.incoming.info('Will evaluate script of Trigger', integration.name);
  		logger.incoming.debug(script);
  
  		const vmScript = vm.createScript(script, 'script.js');
  		vmScript.runInNewContext(sandbox);
  		if (sandbox.Script) {
  			compiledScripts[integration._id] = {
  				script: new sandbox.Script(),
  				store,
  				_updatedAt: integration._updatedAt,
  			};
  
  			return compiledScripts[integration._id].script;
  		}
  	} catch ({ stack }) {
  		logger.incoming.error('[Error evaluating Script in Trigger', integration.name, ':]');
  		logger.incoming.error(script.replace(/^/gm, '  '));
  		logger.incoming.error('[Stack:]');
  		logger.incoming.error(stack.replace(/^/gm, '  '));
  		throw API.v1.failure('error-evaluating-script');
  	}
  
  	if (!sandbox.Script) {
  		logger.incoming.error('[Class "Script" not in Trigger', integration.name, ']');
  		throw API.v1.failure('class-script-not-found');
  	}
  }
  
  function createIntegration(options, user) {
  	logger.incoming.info('Add integration', options.name);
  	logger.incoming.debug(options);
  
  	Meteor.runAsUser(user._id, function() {
  		switch (options.event) {
  			case 'newMessageOnChannel':
  				if (options.data == null) {
  					options.data = {};
  				}
  				if ((options.data.channel_name != null) && options.data.channel_name.indexOf('#') === -1) {
  					options.data.channel_name = `#${ options.data.channel_name }`;
  				}
  				return Meteor.call('addOutgoingIntegration', {
  					username: 'rocket.cat',
  					urls: [options.target_url],
  					name: options.name,
  					channel: options.data.channel_name,
  					triggerWords: options.data.trigger_words,
  				});
  			case 'newMessageToUser':
  				if (options.data.username.indexOf('@') === -1) {
  					options.data.username = `@${ options.data.username }`;
  				}
  				return Meteor.call('addOutgoingIntegration', {
  					username: 'rocket.cat',
  					urls: [options.target_url],
  					name: options.name,
  					channel: options.data.username,
  					triggerWords: options.data.trigger_words,
  				});
  		}
  	});
  
  	return API.v1.success();
  }
  
  function removeIntegration(options, user) {
  	logger.incoming.info('Remove integration');
  	logger.incoming.debug(options);
  
  	const integrationToRemove = Models.Integrations.findOne({
  		urls: options.target_url,
  	});
  
  	Meteor.runAsUser(user._id, () => Meteor.call('deleteOutgoingIntegration', integrationToRemove._id));
  
  	return API.v1.success();
  }
  
  function executeIntegrationRest() {
  	logger.incoming.info('Post integration:', this.integration.name);
  	logger.incoming.debug('@urlParams:', this.urlParams);
  	logger.incoming.debug('@bodyParams:', this.bodyParams);
  
  	if (this.integration.enabled !== true) {
  		return {
  			statusCode: 503,
  			body: 'Service Unavailable',
  		};
  	}
  
  	const defaultValues = {
  		channel: this.integration.channel,
  		alias: this.integration.alias,
  		avatar: this.integration.avatar,
  		emoji: this.integration.emoji,
  	};
  
  	if (this.integration.scriptEnabled && this.integration.scriptCompiled && this.integration.scriptCompiled.trim() !== '') {
  		let script;
  		try {
  			script = getIntegrationScript(this.integration);
  		} catch (e) {
  			logger.incoming.warn(e);
  			return API.v1.failure(e.message);
  		}
  
  		this.request.setEncoding('utf8');
  		const content_raw = this.request.read();
  
  		const request = {
  			url: {
  				hash: this.request._parsedUrl.hash,
  				search: this.request._parsedUrl.search,
  				query: this.queryParams,
  				pathname: this.request._parsedUrl.pathname,
  				path: this.request._parsedUrl.path,
  			},
  			url_raw: this.request.url,
  			url_params: this.urlParams,
  			content: this.bodyParams,
  			content_raw,
  			headers: this.request.headers,
  			body: this.request.body,
  			user: {
  				_id: this.user._id,
  				name: this.user.name,
  				username: this.user.username,
  			},
  		};
  
  		try {
  			const { sandbox } = buildSandbox(compiledScripts[this.integration._id].store);
  			sandbox.script = script;
  			sandbox.request = request;
  
  			const result = Future.fromPromise(vm.runInNewContext(`
  				new Promise((resolve, reject) => {
  					Fiber(() => {
  						scriptTimeout(reject);
  						try {
  							resolve(script.process_incoming_request({ request: request }));
  						} catch(e) {
  							reject(e);
  						}
  					}).run();
  				}).catch((error) => { throw new Error(error); });
  			`, sandbox, {
  				timeout: 3000,
  			})).wait();
  
  			if (!result) {
  				logger.incoming.debug('[Process Incoming Request result of Trigger', this.integration.name, ':] No data');
  				return API.v1.success();
  			} if (result && result.error) {
  				return API.v1.failure(result.error);
  			}
  
  			this.bodyParams = result && result.content;
  			this.scriptResponse = result.response;
  			if (result.user) {
  				this.user = result.user;
  			}
  
  			logger.incoming.debug('[Process Incoming Request result of Trigger', this.integration.name, ':]');
  			logger.incoming.debug('result', this.bodyParams);
  		} catch ({ stack }) {
  			logger.incoming.error('[Error running Script in Trigger', this.integration.name, ':]');
  			logger.incoming.error(this.integration.scriptCompiled.replace(/^/gm, '  '));
  			logger.incoming.error('[Stack:]');
  			logger.incoming.error(stack.replace(/^/gm, '  '));
  			return API.v1.failure('error-running-script');
  		}
  	}
  
  	// TODO: Turn this into an option on the integrations - no body means a success
  	// TODO: Temporary fix for https://github.com/RocketChat/Rocket.Chat/issues/7770 until the above is implemented
  	if (!this.bodyParams || (_.isEmpty(this.bodyParams) && !this.integration.scriptEnabled)) {
  		// return RocketChat.API.v1.failure('body-empty');
  		return API.v1.success();
  	}
  
  	this.bodyParams.bot = { i: this.integration._id };
  
  	try {
  		const message = processWebhookMessage(this.bodyParams, this.user, defaultValues, this.integration);
  		if (_.isEmpty(message)) {
  			return API.v1.failure('unknown-error');
  		}
  
  		if (this.scriptResponse) {
  			logger.incoming.debug('response', this.scriptResponse);
  		}
  
  		return API.v1.success(this.scriptResponse);
  	} catch ({ error, message }) {
  		return API.v1.failure(error || message);
  	}
  }
  
  function addIntegrationRest() {
  	return createIntegration(this.bodyParams, this.user);
  }
  
  function removeIntegrationRest() {
  	return removeIntegration(this.bodyParams, this.user);
  }
  
  function integrationSampleRest() {
  	logger.incoming.info('Sample Integration');
  	return {
  		statusCode: 200,
  		body: [
  			{
  				token: Random.id(24),
  				channel_id: Random.id(),
  				channel_name: 'general',
  				timestamp: new Date(),
  				user_id: Random.id(),
  				user_name: 'rocket.cat',
  				text: 'Sample text 1',
  				trigger_word: 'Sample',
  			}, {
  				token: Random.id(24),
  				channel_id: Random.id(),
  				channel_name: 'general',
  				timestamp: new Date(),
  				user_id: Random.id(),
  				user_name: 'rocket.cat',
  				text: 'Sample text 2',
  				trigger_word: 'Sample',
  			}, {
  				token: Random.id(24),
  				channel_id: Random.id(),
  				channel_name: 'general',
  				timestamp: new Date(),
  				user_id: Random.id(),
  				user_name: 'rocket.cat',
  				text: 'Sample text 3',
  				trigger_word: 'Sample',
  			},
  		],
  	};
  }
  
  function integrationInfoRest() {
  	logger.incoming.info('Info integration');
  	return {
  		statusCode: 200,
  		body: {
  			success: true,
  		},
  	};
  }
  
  class WebHookAPI extends APIClass {
  	/* Webhooks are not versioned, so we must not validate we know a version before adding a rate limiter */
  	shouldAddRateLimitToRoute(options) {
  		const { rateLimiterOptions } = options;
  		return (typeof rateLimiterOptions === 'object' || rateLimiterOptions === undefined) && !process.env.TEST_MODE && Boolean(defaultRateLimiterOptions.numRequestsAllowed && defaultRateLimiterOptions.intervalTimeInMS);
  	}
  
  	shouldVerifyRateLimit(/* route */) {
  		return settings.get('API_Enable_Rate_Limiter') === true
  			&& (process.env.NODE_ENV !== 'development' || settings.get('API_Enable_Rate_Limiter_Dev') === true);
  	}
  
  	/*
  	There is only one generic route propagated to Restivus which has URL-path-parameters for the integration and the token.
  	Since the rate-limiter operates on absolute routes, we need to add a limiter to the absolute url before we can validate it
  	*/
  	enforceRateLimit(objectForRateLimitMatch, request, response, userId) {
  		const { method, url } = request;
  		const route = url.replace(`/${ this.apiPath }`, '');
  		const nameRoute = this.getFullRouteName(route, [method.toLowerCase()]);
  		// We'll be creating rate limiters on demand (when validating for the first time).
  		// This is possible since *all* integration hooks should be rate limited the same way.
  		// This way, we'll not have to add new limiters as new integrations are added
  		if (!this.getRateLimiter(nameRoute)) {
  			this.addRateLimiterRuleForRoutes({
  				routes: [route],
  				rateLimiterOptions: defaultRateLimiterOptions,
  				endpoints: {
  					post: executeIntegrationRest,
  					get: executeIntegrationRest,
  				},
  			});
  		}
  
  		const integrationForRateLimitMatch = objectForRateLimitMatch;
  		integrationForRateLimitMatch.route = nameRoute;
  
  		super.enforceRateLimit(integrationForRateLimitMatch, request, response, userId);
  	}
  }
  
  const Api = new WebHookAPI({
  	enableCors: true,
  	apiPath: 'hooks/',
  	auth: {
  		user() {
  			const payloadKeys = Object.keys(this.bodyParams);
  			const payloadIsWrapped = (this.bodyParams && this.bodyParams.payload) && payloadKeys.length === 1;
  			if (payloadIsWrapped && this.request.headers['content-type'] === 'application/x-www-form-urlencoded') {
  				try {
  					this.bodyParams = JSON.parse(this.bodyParams.payload);
  				} catch ({ message }) {
  					return {
  						error: {
  							statusCode: 400,
  							body: {
  								success: false,
  								error: message,
  							},
  						},
  					};
  				}
  			}
  
  			this.integration = Models.Integrations.findOne({
  				_id: this.request.params.integrationId,
  				token: decodeURIComponent(this.request.params.token),
  			});
  
  			if (!this.integration) {
  				logger.incoming.info('Invalid integration id', this.request.params.integrationId, 'or token', this.request.params.token);
  
  				return {
  					error: {
  						statusCode: 404,
  						body: {
  							success: false,
  							error: 'Invalid integration id or token provided.',
  						},
  					},
  				};
  			}
  
  			const user = Models.Users.findOne({
  				_id: this.integration.userId,
  			});
  
  			return { user };
  		},
  	},
  });
  
  Api.addRoute(':integrationId/:userId/:token', { authRequired: true }, {
  	post: executeIntegrationRest,
  	get: executeIntegrationRest,
  });
  
  Api.addRoute(':integrationId/:token', { authRequired: true }, {
  	post: executeIntegrationRest,
  	get: executeIntegrationRest,
  });
  
  Api.addRoute('sample/:integrationId/:userId/:token', { authRequired: true }, {
  	get: integrationSampleRest,
  });
  
  Api.addRoute('sample/:integrationId/:token', { authRequired: true }, {
  	get: integrationSampleRest,
  });
  
  Api.addRoute('info/:integrationId/:userId/:token', { authRequired: true }, {
  	get: integrationInfoRest,
  });
  
  Api.addRoute('info/:integrationId/:token', { authRequired: true }, {
  	get: integrationInfoRest,
  });
  
  Api.addRoute('add/:integrationId/:userId/:token', { authRequired: true }, {
  	post: addIntegrationRest,
  });
  
  Api.addRoute('add/:integrationId/:token', { authRequired: true }, {
  	post: addIntegrationRest,
  });
  
  Api.addRoute('remove/:integrationId/:userId/:token', { authRequired: true }, {
  	post: removeIntegrationRest,
  });
  
  Api.addRoute('remove/:integrationId/:token', { authRequired: true }, {
  	post: removeIntegrationRest,
  });
  