private async Task StartNewSurveyAsync(string phoneNumber, ILambdaLogger logger)
        {
            // save info about the respondent
            logger.LogLine($"Saving survey response for {Sanitizer.MaskPhoneNumber(phoneNumber)}");
            var phoneNumberHash = Sanitizer.HashPhoneNumber(phoneNumber);
            var surveyResponse  = new SurveyResponse
            {
                PhoneNumberHash = phoneNumberHash,
                TaskToken       = "",
                Responses       = new Dictionary <string, string>()
            };
            await _DataAccess.SaveSurveyResponeAsync(surveyResponse).ConfigureAwait(false);

            // start the workflow
            logger.LogLine($"Starting workflow for {Sanitizer.MaskPhoneNumber(phoneNumber)}");
            using (var client = StepFunctionClientFactory.GetClient())
            {
                var state = new State
                {
                    PhoneNumber = phoneNumber
                };
                var req = new StartExecutionRequest
                {
                    Name            = phoneNumberHash + Guid.NewGuid().ToString("N"),
                    StateMachineArn = ServerlessConfig.StateMachineArn,
                    Input           = JsonConvert.SerializeObject(state)
                };
                await client.StartExecutionAsync(req).ConfigureAwait(false);
            }
        }
        /// <summary>
        /// Receive an SMS message.
        /// </summary>
        public async Task <APIGatewayProxyResponse> ReceiveSmsAsync(APIGatewayProxyRequest request, ILambdaContext context)
        {
            var logger = context.Logger;

            logger.LogLine("Receiving SMS");

            // API Gateway isn't nice like MVC. We need to parse the form ourselves.
            var formValues = GetFormValues(request);

            if (!TwilioRequestValidator.IsValidPostRequest(request, formValues))
            {
                throw new PreTalkSurveyException("Unable to validate signature on request");
            }

            // Get the SMS message from the form and make sure it comes from the expected phone number.
            var sms = GetSmsMessage(formValues);

            if (sms.To != ServerlessConfig.TwilioPhoneNumber)
            {
                throw new PreTalkSurveyException("Invalid incoming phone number");
            }

            // figure out what to do with the incoming message.
            try
            {
                var surveyResponse = await _DataAccess.GetSurveyResponseAsync(Sanitizer.HashPhoneNumber(sms.From)).ConfigureAwait(false);

                if (surveyResponse == null)
                {
                    // if we've never seen this phone number before, it's the start of a new survey
                    logger.LogLine($"New phone number {Sanitizer.MaskPhoneNumber(sms.From)}, starting new survey");
                    await StartNewSurveyAsync(sms.From, logger).ConfigureAwait(false);
                }

                else if (!string.IsNullOrEmpty(surveyResponse.TaskToken))
                {
                    if (EqualsIgnoreCase(sms.Body, "hello") || EqualsIgnoreCase(sms.Body, "start"))
                    {
                        // if they send "hello" or "start", terminate the workflow and start it again
                        logger.LogLine($"Restarting workflow for {Sanitizer.MaskPhoneNumber(sms.From)}");
                        await FailWaitForResponseAsync(surveyResponse.TaskToken).ConfigureAwait(false);
                        await StartNewSurveyAsync(sms.From, logger).ConfigureAwait(false);
                    }

                    // if there's a task token, we need to advance the state machine
                    var taskToken = surveyResponse.TaskToken;
                    var state     = surveyResponse.SavedState;

                    // evaluate the answer to the question
                    var questionResponse = Survey.EvaluateResponseToQuestion(state.Question, sms.Body.Trim());
                    state.IsValidResponse = questionResponse.IsValidResponse;
                    logger.LogLine($"Evaluated response to question \"{state.Question}\". Response \"{sms.Body}\" from {Sanitizer.MaskPhoneNumber(sms.From)}. IsValidResponse={questionResponse.IsValidResponse};StandardizedResponse={questionResponse.StandardizedResponse}");

                    // clear the token and the saved state -- we want to make sure these values aren't reused
                    // so we save before completing the activity.
                    logger.LogLine($"Clearing state for {Sanitizer.MaskPhoneNumber(sms.From)}");
                    surveyResponse.TaskToken  = "";
                    surveyResponse.SavedState = null;

                    // store the answer
                    if (state.IsValidResponse)
                    {
                        surveyResponse.Responses[questionResponse.Question] = questionResponse.StandardizedResponse;
                    }
                    await _DataAccess.SaveSurveyResponeAsync(surveyResponse);

                    // store the answer to the question

                    // advance the state machine
                    logger.LogLine($"Completing response for task token {taskToken}");
                    await CompleteWaitForResponseAsync(taskToken, state).ConfigureAwait(false);
                }

                else
                {
                    // they must have previously completed the survey - start again
                    logger.LogLine($"Restarting workflow for {Sanitizer.MaskPhoneNumber(sms.From)} - previously completed");
                    await StartNewSurveyAsync(sms.From, logger).ConfigureAwait(false);
                }
            }
            catch
            {
                logger.LogLine($"Sending snarky error message to {Sanitizer.MaskPhoneNumber(sms.From)}");
                await SendErrorMessage(sms.From).ConfigureAwait(false);

                throw;
            }

            logger.LogLine("Returning OK response");
            return(GetOkResponse());
        }
Exemple #3
0
        /// <summary>
        /// Waits for an SMS activity to begin, then handles it.
        /// </summary>
        public async Task WaitSmsAsync(ILambdaContext context)
        {
            var startTime     = DateTime.Now;
            var timeAvailable = TimeSpan.FromMinutes(4);

            using (var client = StepFunctionClientFactory.GetClient())
            {
                TimeSpan timeLeft;
                do
                {
                    context.Logger.LogLine("Polling GetActivityTask");
                    var arn = ServerlessConfig.WaitSmsResponseActivityArn;
                    var req = new GetActivityTaskRequest {
                        ActivityArn = arn
                    };
                    var result = await client.GetActivityTaskAsync(req).ConfigureAwait(false);

                    if (result.Input != null && result.TaskToken != null)
                    {
                        context.Logger.LogLine("Received token");

                        // get the current state for the activity
                        var state = JsonConvert.DeserializeObject <State>(result.Input);

                        // save the task token to the survey response and persist the current state
                        // so we can restore it later.
                        var surveyResponse = await _DataAccess.GetSurveyResponseAsync(Sanitizer.HashPhoneNumber(state.PhoneNumber))
                                             .ConfigureAwait(false);

                        surveyResponse.TaskToken  = result.TaskToken;
                        surveyResponse.SavedState = state;
                        await _DataAccess.SaveSurveyResponeAsync(surveyResponse).ConfigureAwait(false);

                        // ask the question -- do this last so that the current state is persisted with the task token
                        await AskQuestionAsync(state.PhoneNumber, Survey.GetQuestion(state.Question)).ConfigureAwait(false);
                    }

                    timeLeft = timeAvailable - (DateTime.Now - startTime);
                } while (timeLeft.TotalMilliseconds >= 65000);

                context.Logger.LogLine("Polling loop terminated");
            }
        }