* 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();