Sunday, April 13, 2014

Receive SMS Text Message

As part of my investigation into Phones For Seniors, in order to use the Where's My Droid app to locate a phone from a website when that phone has no data access, I need by server to be able to receive SMS Text Messages. That effectively means that my server need it's own phone number. Where's My Droid suggested that I should use Twilio.

I thought this would be achievable with Amazon SNS, but that only supports delivering Amazon SNS messages as text messages to cell phones. After a thorough reading of the SNS FAQ, it appears it doesn't support receiving them.

The Amazon SNS Docs say that you can "send and receive Short Message Service (SMS) notifications". But after reading more it becomes clear that "receive" means "receive on your celphone" not "receive at your EC2 instance".

Your EC2 instance can trigger actions by subscribing to an SQS queue which can receive actions from SNS, but in this case it seems that something inside AWS would trigger the publishing of an SNS which would eventually get to your EC2. There is no mechanism for something outside AWS to send an SMS which triggers something inside AWS.

The fourth hit for trigger sqs from sms is Amazon SQS & Twilio Integrations, so it looks like WMD gave the right advice. And it seems they've supported Canada since 2011.

Twilio

Twilio is fantastic. The signup process is very easy. You just choose a username/password and tell them your phone number. They send you a text or a robo-call to verify you're at that number, then they give you your first Twilio number. That's it. You're now live.

Just give them your credit card and $20 and you'll get to keep the trial number for $1/month.

Their API explorer lets you send a SMS and shows you the PHP (or whatever) necessary to send it.

// this line loads the library 
require('/path/to/twilio-php/Services/Twilio.php'); 
 
$account_sid = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 
$auth_token = '[AuthToken]'; 
$client = new Services_Twilio($account_sid, $auth_token); 
 
$client->account->messages->create(array( 
	'To' => "111-111-1111", 
	'From' => "+12222222222", 
	'Body' => "test",   
));

Your phone (which you don't have to authenticate) will receive the text and their API explorer shows you the JSON response to the API request.

{
  "sid": "SM22222222222222222222222222222222",
  "date_created": "Sun, 13 Apr 2014 20:13:03 +0000",
  "date_updated": "Sun, 13 Apr 2014 20:13:03 +0000",
  "date_sent": null,
  "account_sid": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
  "to": "+11111111111",
  "from": "+12222222222",
  "body": "test",
  "status": "queued",
  "num_segments": "1",
  "num_media": "0",
  "direction": "outbound-api",
  "api_version": "2010-04-01",
  "price": null,
  "price_unit": "USD",
  "uri": "/2010-04-01/Accounts/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/Messages/SM22222222222222222222222222222222.json",
  "subresource_uris": {
    "media": "/2010-04-01/Accounts/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/Messages/SM22222222222222222222222222222222/Media.json"
  }
}

Download Twilio.php and try it for yourself. I believe you need the entire services folder. I tweaked it a bit based on their API Docs. You also need to get your auth-token from the dashboard by clicking on the little lock icon beside the auth-token heading near the top right.

<?
session_start();
if ($_SESSION['secret'] != '111111111111111')
{
  header('Content-type: application/json');
  echo '{"error":"not authenticated"}';
  exit;
}

require('twilio/Twilio.php'); 
 
$account_sid = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 
$auth_token = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; 
$client = new Services_Twilio($account_sid, $auth_token); 

header('Content-type: application/json');
try
{
  $sms = $client->account->sms_messages->create(
    "111-111-1111", // From this number
    "222-222-2222", // To this number
    "WMD GPS"       // This message
  ); 
  echo '{"sent message":'.json_encode($sms->sid).'}';
}
catch (Services_Twilio_RestException $e)
{
  echo '{"error":'.json_encode($e->getMessage()).'}';
}
?>

The default config seems to be that when twilo receives an SMS from a phone to your twilo number, it caches that message for you and automatically replies to the phone thanking them for the message. I'll need to access that cache and disable that automatic reply.

It looks like you have to configure a messaging URL, and when your twilio number receives an SMS, they make a request to that url with the message details and take action based on your response.

You configure the request URL for voice and messaging by clicking the numbers tab in your twilio account and selecting your number. The default urls are what caused the auto-reply. To indicate that you don't want twilio to take any further action, just provide an empty response.

<?
  header('Content-type: text/xml');
  echo '<Response></Response>';
?>

Here's a simple request URL for texts that will save the message to your server.

<?
  $body = $_POST['Body'];
  file_put_contents('/tmp/sms.txt','{"stamp":'.time().',"body":'.json_encode($body).'}');
  header('Content-type: text/xml');
  echo '<Response></Response>';
?>

Simple Twilio/Where'sMyDroid App

If, like me, you want an SMS-only commander interface to lookup your phone using the Where'sMyDroid App, you can setup the following simple PHP app with Twilio to achieve that effect. I'm assuming these files are rooted at http://yoursite.com/wmd/ and that the Twilio sdk is at http://yoursite.com/wmd/twilio/ and that you've registered your Twilio request URLs as http://yoursite.com/wmd/cmd_voice.php and http://yoursite.com/wmd/cmd_text.php respectively.

util.js

//////////////////////////////////////////////////////////////////////////////////

var util = {};

//////////////////////////////////////////////////////////////////////////////////

/**
 * Send an ajax request. If it fails, report via util.err();
 *
 * operation -- Friendly description of this operation for use in error messages, as in "Failed to ..."
 * action    -- Function to execute when we get a response back. Will be passed the responseText.
 * kind      -- "GET" or "POST"
 * url       -- in the case of "GET", all parameters are in the query string
 * data      -- only required for "POST", must be a form.elements object
 */
util.ajax = function( operation, action, kind, url, data )
{
  if ( typeof action != "function" )
  {
    alert("Invalid use of util.ajax(); Requires a function to handle the result.");
    return;
  }
  if (kind != "GET" && kind != "POST")
  {
    alert("Invalid use of util.ajax(); Requires GET or POST.");
    return;
  }
  if (url == null || url == "")
  {
    alert("Invalid use of util.ajax(); Requires a url.");
    return;
  }

  var xhr;
  try { xhr = new XMLHttpRequest(); } // Firefox, Safari
  catch (e) {
    try { xhr = new ActiveXObject('Msxml2.XMLHTTP'); }
    catch (e2) { xhr = new ActiveXObject('Microsoft.XMLHTTP'); }
  }
  if (xhr == null)
  {
    alert("Failed to "+operation+" becasue your browser doesn't support AJAX.");
    return;
  }

  xhr.open(kind, url, true);
  xhr.onreadystatechange = function()
  { 
    if (this.readyState == 4)
    {
      if (this.status == 200) 
      {
        action(xhr.responseText);
      }
      //else { util.err("Failed to "+operation+". Error Code: "+xhr.status); }
    }
  };
  var body = null;
  if ( kind == "POST" )
  {
    xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
    body = "";
    var amp = "";
    for (var i=0; i<data.length; i++)
    {
      // "&name=value"
      body += amp+encodeURIComponent(data[i].name)+"="+encodeURIComponent(data[i].value);
      if (i==0) { amp = "&"; }
    }
    xhr.setRequestHeader("Content-length",body.length);
    xhr.setRequestHeader("Connection","close");
  }
  xhr.send(body);
};

//////////////////////////////////////////////////////////////////////////////////

/**
 * Make input safe to display in html.
 * http://lawrence.ecorp.net/inet/samples/regexp-intro.php
 */
util.hFix = function(txt)
{
  var safe = ''+txt;
  safe = safe.replace(/&/g,"&amp;");
  safe = safe.replace(/>/g,"&gt;");
  safe = safe.replace(/</g,"&lt;");
  return safe;
};

//////////////////////////////////////////////////////////////////////////////////

index.php

<?
session_start();

/**
 * Make safe to include in a double quoted html string constant.
 * <input type="text" value="<?=tFix($val)?>"/>
 *
 * If a default is provided and input is blank then use the default.
 */
function tFix($val, $default="")
{
  $find_ary    = array( "&"    , ">"   , "<"   , "\""    , "\r", "\n" );
  $replace_ary = array( "&amp;", "&gt;", "&lt;", "&quot;",  " ",  " " );

  $val = trim($val);
  if ($val == "") { $val = trim($default); }
  return str_replace($find_ary, $replace_ary, $val);
}

/**
 * Convert a value to an integer. If conversion fails, return zero.
 * If optional $positive is true then negative numbers return zero.
 */
function SafeInteger($val,$positive=false)
{
  $val = trim($val);
  if (is_numeric($val))
  {
    $val = intval($val);
    if ($positive && $val < 0) { return 0; }
    else { return $val; }
  }    
  return 0;
}

/////////////////////////////////////////////////////////////////
  
if ($_SESSION['secret'] != 'we5jk458g487gt4w85gb')
{
  ?>
<!DOCTYPE html>
<html>
<head>
  <title>Login</title>
</head>
<body>
  Password:
  <form method="post" action="cmd_login.php">
  <input type="text" name="secret"/>
  <input type="submit" value="Go"/>
  </form>
</body>
</html>
  <?
}
else ////////////////////////////////////////////////////////////
{
  ?>
<!DOCTYPE html>
<html>
<head>
  <title>Location</title>
  <script src="util.js" type="text/javascript"></script>
</head>
<body>
  <input type="button" onclick="wmd()" value="Find It"/>
  <div id="msg"></div>
<?
  $stamp = 0;
  $sms = file_get_contents('/tmp/sms.txt'); 
  if ($sms != false)
  {
    $sms = json_decode($sms,true);
    $stamp = $sms['stamp'];
    if ($sms['stamp'] != null && $sms['body'] != null)
    {
      echo '<br/>'.date('l jS \of F Y h:i:s A',$sms['stamp']-60*60*4).' EST';
      echo '<br/><iframe style="border: 0; width: 100%; height: 600px;" src="'.tFix($sms['body']).'&output=embed"></iframe>'; 
    }
  }
?>
</body>
<script>
var msg = document.getElementById('msg');
var dots = '';
var stamp = <?=SafeInteger($stamp)?>;
var processing = false;

var wmd = function()
{
  if (processing) { return; }
  processing = true;
  util.ajax( 'wmd', wmdFinished, 'GET', 'cmd_send.php' );
};
var wmdFinished = function(responseText)
{
  msg.innerHTML = util.hFix(responseText);
  look();
};
var look = function()
{
  util.ajax( 'look', lookFinished, 'GET', 'cmd_look.php' );
};
var lookFinished = function(responseText)
{    
  msg.innerHTML = 'Looking'+dots;
  dots = dots + '.';
  if (responseText != null)
  {
    var parsed = JSON.parse(responseText);
    if (parsed.stamp != stamp)
    { document.location = "./"; }
  }
  setTimeout(look,5000);
};
</script>
</html>
  <?
}
?>

cmd_login.php

<?
session_start();

if ($_POST['secret'] == 'yourSitePassword')
{ $_SESSION['secret'] = 'we5jk458g487gt4w85gb'; }

header('Location: http://yoursite.com/wmd/');
?>

cmd_send.php

<?
session_start();
if ($_SESSION['secret'] != 'we5jk458g487gt4w85gb')
{
  header('Content-type: application/json');
  echo '{"error":"not authenticated"}';
  exit;
}

require('twilio/Twilio.php'); 
 
$account_sid = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 
$auth_token = 'ffffffffffffffffffffffffffffffff'; 
$client = new Services_Twilio($account_sid, $auth_token); 

header('Content-type: application/json');
try
{
  $sms = $client->account->sms_messages->create(
    "111-111-1111", // From this number
    "222-222-2222", // To this number
    "WMD GPS"       // This message
  ); 
  $msg = '{"sent message":'.json_encode($sms->sid).'}';
  echo $msg;
}
catch (Services_Twilio_RestException $e)
{
  echo '{"error":'.json_encode($e->getMessage()).'}';
}
?>

cmd_voice.php

<?
  header('Content-type: text/xml');
  echo '<Response></Response>';
?>

cmd_text.php

<?
  $body = $_POST['Body'];
  file_put_contents('/tmp/sms.txt','{"stamp":'.time().',"body":'.json_encode($body).'}');
  header('Content-type: text/xml');
  echo '<Response></Response>';
?>

cmd_look.php

<?
  $sms = file_get_contents('/tmp/sms.txt');
  header('Content-type: application/json');
  echo $sms;
?>
web
{ "loggedin": false, "owner": false, "avatar": "", "render": "nothing", "trackingID": "UA-36983794-1", "description": "", "page": { "blogIds": [ 493 ] }, "domain": "holtstrom.com", "base": "\/michael", "url": "https:\/\/holtstrom.com\/michael\/", "frameworkFiles": "https:\/\/holtstrom.com\/michael\/_framework\/_files.4\/", "commonFiles": "https:\/\/holtstrom.com\/michael\/_common\/_files.3\/", "mediaFiles": "https:\/\/holtstrom.com\/michael\/media\/_files.3\/", "tmdbUrl": "http:\/\/www.themoviedb.org\/", "tmdbPoster": "http:\/\/image.tmdb.org\/t\/p\/w342" }