* Copyright © 2013 Strangecode, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * sms-response.php * * @author Quinn Comendant * @version 1.0 * @since 02 Nov 2013 18:39:36 */ /******************************************************************** * CONFIG ********************************************************************/ require_once dirname(__FILE__) . '/../_config.inc.php'; require_once 'models/SMS.inc.php'; require_once 'models/Survey.inc.php'; /******************************************************************** * MAIN ********************************************************************/ // Values received from the API webhook. $sms = SMS::receive(); if ('' == trim($sms['sender_number']) || '' == trim($sms['text'])) { // Empty request; ignore. header($_SERVER['SERVER_PROTOCOL'] . ' 501 Not Implemented', true, 501); $app->stop(); die('Bad request'); } // Process multiple-part messages. // False is returned here if we need to wait for additional parts to arrive. Otherwise, $sms array contains the final, usable data. if (false === $sms = SMS::processMultipart($sms)) { header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK', true, 200); $app->stop(); die('Waiting for additional message parts'); } // Parse received SMS for special keywords. switch (strtolower(trim($sms['text']))) { case 'join' : case 'unete' : // Add survey participant. Participant::optIn($sms['sender_number'], $sms['virtual_number']); break; case 'exit' : case 'salir' : // Opt-out survey participant. Participant::optOut($sms['sender_number'], $sms['virtual_number']); break; default: // Continue as survey participant. break; } // Get participant details, if the phone number matches one. if (!$p = Participant::get(array('phone' => $sms['sender_number']))) { $app->logMsg(sprintf('Received a message from unknown sender: %s', $sms['sender_number']), LOG_NOTICE, __FILE__, __LINE__); $app->stop(); die; } // Participants who are 'opted out' are not participating in any surveys; skip. if ('opted out' == $p['status']) { $app->logMsg(sprintf('Received a message from an opted out sender, participant_id: %s', $p['participant_id']), LOG_NOTICE, __FILE__, __LINE__); $app->stop(); die; } // We have response from a real, not-opted-out participant: let's update their last_active_datetime and mark them as 'active'. $db->query(" UPDATE participant_tbl SET status = 'active', last_active_datetime = NOW() WHERE participant_id = '" . $db->escapeString($p['participant_id']) . "' "); // Get the details for the current survey. if (!$s = Survey::getCurrent()) { // No survey found. Closed or not yet begun? In any case we are not accepting responses. $app->logMsg(sprintf('Could not find a survey for participant_id %s response: %s ', $p['participant_id'], $sms['text']), LOG_NOTICE, __FILE__, __LINE__); $app->stop(); die; } // Check participant survey status. if (!$p_s_status = Participant::getSurveyStatus($p['participant_id'], $s['survey_id'])) { // User has not yet joined any surveys; skip them. $app->logMsg(sprintf('User has not yet joined any surveys; skip them', null), LOG_DEBUG, __FILE__, __LINE__); $app->stop(); die; } else { switch ($p_s_status['status']) { case 'pending' : // Participant hasn't received any questions yet. $app->logMsg(sprintf('Participant hasn\'t received any questions yet.', null), LOG_DEBUG, __FILE__, __LINE__); $app->stop(); die; case 'completed' : // User has finished this survey; skip them. $app->logMsg(sprintf('User has finished this survey; skip them.', null), LOG_DEBUG, __FILE__, __LINE__); $app->stop(); die; case 'underway' : // Ok! We're expecting a response. break; } } // Discover which question this reply is for. if (!$q = Participant::getCurrentQuestion($p['participant_id'])) { // Couldn't find a question for this participant, because this participant is not part of a survey that is underway. // Did the user send a reply BEFORE any surveys have begun sending questions? $app->stop(); die; } if (!Participant::wasSentQuestion($p['participant_id'], $q['question_id'])) { // The next unanswered question hasn't yet been sent yet. We therefore don't have a question to apply this response to. (Duplicate response?) $app->logMsg(sprintf('No sent unanswered questions for participant_id %s (next question_id %s)', $p['participant_id'], $q['question_id']), LOG_DEBUG, __FILE__, __LINE__); $app->stop(); die; } // Ensure the answer is valid for the question format. if (!Question::validateResponse($q['question_id'], $sms['text'])) { // Invalid response; send a reply back to the human to say their response was not valid. $app->logMsg(sprintf("Response (%s) doesn't conform to question format: %s", $sms['text'], $q['question_text']), LOG_NOTICE, __FILE__, __LINE__); // TODO: should we respond uniquely when the last question was a statement? SMS::send($sms['sender_number'], 'Tu respuesta es invalida'); // TODO: we need to make this multilingual. Migrate this into the Survey settings. $app->stop(); die; } // Ensure the account_id matches between the survey, question, and participant. if ($s['account_id'] != $q['account_id'] || $q['account_id'] != $p['account_id']) { $app->logMsg(sprintf('Account mismatch between survey_id %s != question_id %s != participant_id %s ', $s['survey_id'], $q['question_id'], $p['participant_id']), LOG_ERR, __FILE__, __LINE__); $app->stop(); die; } // OK! It's a valid answer. Save it and process actions. $response_id = Response::insert(Response::merge(array( 'account_id' => $s['account_id'], 'participant_id' => $p['participant_id'], 'question_id' => $q['question_id'], 'response' => $sms['text'], 'response_parsed' => Question::getParsedResponse($q['question_id'], $sms['text']), ))); $app->logMsg(sprintf('Saved response_id %s from participant_id %s for question_id %s: %s', $response_id, $p['participant_id'], $q['question_id'], $sms['text']), LOG_INFO, __FILE__, __LINE__); // Apply a question action, if one is defined. if (!$q_action = Question::getAction($q['question_id'], $sms['text'])) { // We didn't find an action; default to 'continue'. $q_action = array( 'question_action' => 'continue', 'target_question_id' => '', ); } $app->logMsg(sprintf('Response %s for question_id %s triggers action: %s %s', $sms['text'], $q['question_id'], $q_action['question_action'], $q_action['target_question_id']), LOG_DEBUG, __FILE__, __LINE__); switch ($q_action['question_action']) { case 'end' : // Explicit end of the survey for this user. Survey::endParticipant($p['participant_id'], $s['survey_id']); $app->stop(); die; case 'continue' : // Go to the next question. if (!$next_q = Question::getNextInSurvey($q['question_id'], $s['survey_id'])) { // No more questions; implicit end of survey. Survey::endParticipant($p['participant_id'], $s['survey_id']); $app->stop(); die; } $next_question_id = $next_q['question_id']; break; case 'goto' : // Go to the specified question. $next_question_id = $q_action['target_question_id']; if (!$next_question_id) { // No more questions; unexpected end of survey. Survey::endParticipant($p['participant_id'], $s['survey_id']); $app->stop(); die; } break; } // Set current_question_id to the next question. $db->query(" UPDATE participant_survey_tbl SET current_question_id = '" . $db->escapeString($next_question_id) . "' WHERE participant_id = '" . $db->escapeString($p['participant_id']) . "' AND survey_id = '" . $db->escapeString($s['survey_id']) . "' "); $app->logMsg(sprintf('Set current_question_id to %s for participant_id %s', $next_question_id, $p['participant_id']), LOG_INFO, __FILE__, __LINE__); // DONE! $app->logMsg(sprintf('Finished processing SMS request', null), LOG_INFO, __FILE__, __LINE__); $app->stop();