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()); }
/// <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"); } }