text: // handle the "POST /no-auth/provider-action/:provider" request to handle a user action
  // initiated for a particular provider
  
  'use strict';
  const { UserState } = require('botbuilder');
  const RestfulRequest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/restful_request.js');
  const ProviderDisplayNames = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/web/provider_display_names');
  const SlackInteractiveComponentsHandler = require('./slack_interactive_components_handler');
  const crypto = require('crypto');
  const AddTeamPublisher = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/users/add_team_publisher');
  const MSTeamsConversationBot = require('./msteams_conversation_bot');
  const { createMSTeamsBotFrameworkAdapter } = require('./msteams_bot_framework_adapter');
  const MSTeamsDatabaseAdapter = require('./msteams_database_adapter');
  const MSTeamsStateAdapter = require('./msteams_state_adapter');
  
  const CODE_PROVIDERS = {
  	github: 'GitHub',
  	gitlab: 'GitLab',
  	bitBucket: 'Bitbucket',
  	'azure-devops': 'Azure DevOps',
  	vsts: 'Azure DevOps'
  };
  
  class ProviderActionRequest extends RestfulRequest {
  	constructor (options) {
  		super(options);
  	}
  
  	async authorize () {
  		if (this.request.params.provider.toLowerCase() === 'slack') {
  			if (!this.verifySlackRequest(this.request, this.request.body.payloadRaw)) {
  				this.log('Slack verification failed');
  				throw this.errorHandler.error('notFound');
  			}
  			// in the success case we don't need this anymore and requireAndAllow will warn for it
  			delete this.request.body.payloadRaw;
  		}
  	}
  
  	// require certain parameters, discard unknown parameters
  	async requireAndAllow () {
  		await this.requireAllowParameters('body', {
  			required: {
  				object: ['payload']
  			}
  		});
  	}
  
  	// process the request...
  	async process () {
  		let data;
  		this.provider = this.request.params.provider.toLowerCase();
  		if (this.provider === 'msteams') {
  			const msTeamsBotFrameworkAdapter = await createMSTeamsBotFrameworkAdapter(this.request);
  			await msTeamsBotFrameworkAdapter.processActivity(this.request, this.response, async (context) => {
  				// we MUST instantiate this adapter for each request
  				this.handler = new MSTeamsDatabaseAdapter(this);
  				context.turnState.set('cs_analytics', this.api.services.analytics);
  				context.turnState.set('cs_databaseAdapter', this.handler);
  				context.turnState.set('cs_logger', this.api.logger);
  				context.turnState.set('cs_requestId', this.request.id);
  				context.turnState.set('cs_stateAdapter', new UserState(new MSTeamsStateAdapter(this)));
  				await MSTeamsConversationBot.run(context);
  			});
  			// we need the response to go back to MS
  			this.responseHandled = true;
  		}
  		else if (this.provider === 'slack') {
  			await this.requireAndAllow(); // require certain parameters, discard unknown parameters
  			data = this.parseSlackActionInfo();
  			if (data) {
  				this.handler = new SlackInteractiveComponentsHandler(
  					this,
  					data
  				);
  			}
  		}
  
  		if (this.handler) {
  			this.results = await this.handler.process();
  			if (this.results) {
  				if (this.results.responseData) {
  					// if we have data that we need to send back to the provider...
  					this.responseData = this.results.responseData;
  				}
  				if (this.results.actionTeam) {
  					const company = await this.getCompany(
  						this.results.actionTeam
  					);
  					await this.sendTelemetry(
  						data,
  						this.results.actionUser,
  						this.results.payloadUserId,
  						this.results.actionTeam,
  						company,
  						this.results.error
  					);
  				}
  				if (this.results.error) {
  					// if we got an error -- re-throw it to prevent any postProcessing
  					// or data saving from happening (like posts getting saved)
  					throw new Error(this.results.error.reason || 'GenericError');
  				}
  			}
  		}
  	}
  
  	verifySlackRequest (request, rawBody) {
  		// mainly snagged from https://medium.com/@rajat_sriv/verifying-requests-from-slack-using-node-js-69a8b771b704
  		try {
  			if (!request.body || !rawBody || !request.body.payload) return false;
  
  			const apiAppId = request.body.payload.api_app_id;
  			if (!apiAppId) return false;
  
  			const slackSigningSecret = this.api.config.integrations.slack.signingSecretsByAppIds[apiAppId];
  			if (!slackSigningSecret) {
  				this.api.log(`Could not find signingSecret for appId=${apiAppId}`);
  				return false;
  			}
  
  			const slackSignature = request.headers['x-slack-signature'];
  			const timestamp = request.headers['x-slack-request-timestamp'];
  			if (!slackSignature || !timestamp) return false;
  
  			// protect against replay attacks
  			const time = Math.floor(new Date().getTime() / 1000);
  			if (Math.abs(time - timestamp) > 300) return false;
  
  			const mySignature = 'v0=' +
  				crypto.createHmac('sha256', slackSigningSecret)
  					.update('v0:' + timestamp + ':' + rawBody, 'utf8')
  					.digest('hex');
  
  			return crypto.timingSafeEqual(
  				Buffer.from(mySignature, 'utf8'),
  				Buffer.from(slackSignature, 'utf8'));
  		}
  		catch (ex) {
  			this.api.log(`verifySlackRequest error. ${ex}`);
  		}
  		return false;
  	}
  
  	// parse the action info within the given payload
  	parseSlackActionInfo () {
  		const payload = this.request.body.payload;
  
  		let actionPayload;
  		let actionOrCallbackId =
  			payload.actions &&
  			payload.actions[0] &&
  			payload.actions[0].action_id;
  		if (!actionOrCallbackId) {
  			actionOrCallbackId = payload.view && payload.view.private_metadata;
  		}
  		if (!actionOrCallbackId) {
  			this.log(
  				`Could not find action_id within the ${this.provider} payload`
  			);
  			return undefined;
  		}
  
  		try {
  			actionPayload = JSON.parse(actionOrCallbackId);
  		} catch (error) {
  			const message =
  				error instanceof Error ? error.message : JSON.stringify(error);
  			this.log(
  				`Unable to parse action_id sent with ${this.provider} payload: ${message}`
  			);
  			return undefined;
  		}
  		return {
  			payload: payload,
  			actionPayload: actionPayload
  		};
  	}
  
  	// get the company that owns the team
  	async getCompany (team) {
  		return await this.data.companies.getById(team.get('companyId'));
  	}
  
  	// send telemetry event associated with this action
  	async sendTelemetry (data, user, providerUserId, team, company, error) {
  		if (!data || !data.actionPayload || (!user && !providerUserId) || !team) return;
  		const endpoint =
  			this.provider === 'slack'
  				? 'Slack'
  				: this.provider === 'msteams'
  					? 'MSTeams'
  					: this.provider;
  
  		const info = this.getTrackingInfo(data.payload, data.actionPayload, endpoint, error);
  		if (!info) {
  			this.log(
  				`Could not get tracking info from ${endpoint} payload`
  			);
  			return false;
  		}
  
  		const trackData = {
  			Endpoint: endpoint
  		};
  		if (!user) {
  			trackData.distinct_id = providerUserId;
  		}
  
  		Object.assign(trackData, info.data);
  		this.api.services.analytics.trackWithSuperProperties(
  			info.event,
  			trackData,
  			{
  				request: this,
  				user: user,
  				team: team,
  				company: company,
  				userId: !user && providerUserId
  			}
  		);
  	}
  
  	// get the tracking info associated with this requset
  	getTrackingInfo (payload, actionPayload, endpoint, error) {
  		if (error && error.eventName) {
  			return {
  				event: error.eventName,
  				data: {
  					Error: error.reason,
  					Endpoint: endpoint || ''
  				}
  			};
  		}
  		else if (payload && payload.type === 'view_submission') {
  			return {
  				event: 'Reply Created',
  				data: {
  					CodemarkId: actionPayload && actionPayload.cId,
  					Endpoint: endpoint || ''
  				}
  			};
  		}
  		else if (actionPayload.linkType === 'web') {
  			return {
  				event: 'Opened on Web'
  			};
  		}
  		else if (actionPayload.linkType === 'reply') {
  			return {
  				event: 'View Discussion & Reply',
  				data: {
  					Endpoint: endpoint || ''
  				}
  			};
  		} else if (actionPayload.linkType === 'external') {
  			if (actionPayload.externalType === 'code') {
  				return {
  					event: 'Opened Code',
  					data: {
  						Host:
  							CODE_PROVIDERS[actionPayload.externalProvider || actionPayload.eP] || ''
  					}
  				};
  			} else if (actionPayload.externalType === 'issue') {
  				return {
  					event: 'Opened Issue',
  					data: {
  						Service:
  							ProviderDisplayNames[actionPayload.externalProvider || actionPayload.eP] || ''
  					}
  				};
  			}
  		}
  	}
  
  	async postProcess () {
  		if (this.handler) {
  			if (this.handler.postCreator && this.postPublishData) {
  				await this.handler.postCreator.postCreate({ postPublishData: this.postPublishData });
  			}
  			if (this.handler.createdUser && this.results.actionTeam) {
  				await this.publishAddToTeam(this.results.actionTeam.get('id'));
  			}
  		}
  	}
  
  	// publish to the team that the user has been added,
  	// and publish to the user that they've been added to the team
  	async publishAddToTeam (teamId) {
  		// get the team again since the team object has been modified,
  		// this should just fetch from the cache, not from the database
  		const team = await this.data.teams.getById(teamId);
  		await new AddTeamPublisher({
  			request: this,
  			broadcaster: this.api.services.broadcaster,
  			users: [this.transforms.createdUser],
  			team: team,
  			teamUpdate: this.transforms.teamUpdate,
  			userUpdates: this.transforms.userUpdates
  		}).publishAddedUsers();
  	}
  
  	// describe this route for help
  	static describe () {
  		return {
  			tag: 'provider-action',
  			summary:
  				'Callback indicating a user action within a provider rendering of a post',
  			access: 'No authorization needed',
  			description: 'Provides a callback for whenever a user takes an action within a provider\'s rich rendering of a post',
  			input: {
  				summary: 'Specify payload in the body',
  				looksLike: {
  					'provider*': '<Payload object>'
  				}
  			},
  			returns: 'Empty object',
  			errors: ['parameterRequired']
  		};
  	}
  
  	async handleResponse () {
  		if (this.gotError) {
  			return super.handleResponse();
  		}
  
  		// handle various data transforms that may have occurred as a result of creating the post,
  		// adding objects to the response returned
  		const { transforms } = this;
  		this.postPublishData = this.postPublishData || {};
  
  		if (transforms.streamUpdateForPost) {
  			this.postPublishData.streams = [
  				...(this.postPublishData.streams || []),
  				transforms.streamUpdateForPost
  			];
  		}
  
  		if (transforms.postUpdate) {
  			this.postPublishData.posts = [transforms.postUpdate];
  		}
  
  		// if there are other codemarks updated, add them
  		if (transforms.updatedCodemarks) {
  			this.postPublishData.codemarks = transforms.updatedCodemarks;
  		}
  
  		if (this.handler && this.handler.postCreator) {
  			this.postPublishData.post = this.handler.postCreator.model.getSanitizedObject({
  				request: this
  			});
  		}
  
  		if (transforms.userUpdates) {
  			for (const user of transforms.userUpdates) {
  				const message = {
  					requestId: this.request.id,
  					user: user
  				};
  				const channel = `user-${user.id}`;
  				try {
  					await this.api.services.broadcaster.publish(
  						message,
  						channel,
  						{ request: this }
  					);
  				}
  				catch (error) {
  					// this doesn't break the chain, but it is unfortunate...
  					this.warn(`Could not publish company creation message to user ${user.id}: ${JSON.stringify(error)}`);
  				}
  			}
  
  		}
  
  		if (this.handler && this.handler.createdUser) {
  			this.transforms.createdUser = this.handler.createdUser;
  		}
  
  		// TOOD need this or what?
  		if (this.responseHandled) {
  			return;
  		}
  
  		await super.handleResponse();
  	}
  }
  
  module.exports = ProviderActionRequest;
  