twilio-labs / function-templates

Twilio Function templates for different Voice, SMS, Video, Chat, Sync, etc use cases

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Whisper Funlet

eric-brechemier opened this issue · comments

Migrate the undocumented Whisper Twimlet to an equivalent Twilio Function Template.

Stage 1

Gather a single digit, requested:

  • by playing a sound file from the URL provided in Message, if it starts with http
  • by saying the message provided in Message, if any, using an English voice
  • or by saying a default message in English stating the number of the caller and ending with Press any key to accept.

IF the HumanCheck flag is set
AND no digit has been entered
(gather times out after waiting for 5 seconds more after the end of the message)
THEN hang up

Stage 2

WHEN a digit has been entered (Digits parameter is set and non-empty)
THEN accept the call (return an empty document)

Annotated Source Code

based on a snapshot of Whisper TwiML source code (whisper.php)
sent to me by Twilio support by email on 2019-07-08

This is PHP code.

<?php

It is shared by Twilio under the MIT license.

/*
Copyright (c) 2012 Twilio, Inc.

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/

It uses the (deprecated) class Twilio\Twiml of the twilio-php helper library to generate TwiML.

use Twilio\Twiml;

Start a new TwiML document.

// initiate response library
$response = new Twiml();

Stage 2

// if we have a Digits= parameter, then this is the 2nd loop of this script
if (isset($_REQUEST['Digits'])) {

	// if a digit was pressed, then let us drop through
	if (!strlen($_REQUEST['Digits'])) {

This part seems unreachable: <Gather/> falls through to the rest of the document without triggering the action URL when no digits have been pressed:

However, if the caller did not enter any digits or speech, call flow would continue in the original TwiML document.
https://www.twilio.com/docs/voice/twiml/gather#action

		// no digit was pressed, so just hangup
		$response->hangup();

	}

	// otherwise, we'll just return an empty document, which will bridge the calls

Stage 1

} else {

	// no digits submitted, so this is the first run of the whisper file

	// grab the caller's phone number
	$from = strlen($_REQUEST['From']) ? $_REQUEST['From'] : $_REQUEST['Caller'];

	// add a Gather to get the digits when pressed
	$gather = $response->gather(array(
		'numDigits' => 1,
	));

	// figure out the message
	// first, check to see if we have an http URL (simple check)
	if (strtolower(substr(trim($_GET['Message']), 0, 4)) == 'http') {
		$gather->play($_GET['Message']);
	}

	// check if we have any message, if so, read it back
	elseif (strlen(trim($_GET['Message']))) {
		$gather->say(stripslashes($_GET['Message']));
	}

	// no message, just use a default
	else {
		$gather->say('You are receiving a call from ' . preg_replace('/([^\s])/', '$1. ', $from) . '.  Press any key to accept.');
	}

	// if we're screening to check for a person answering, hangup the call if gather falls through
	if (isset($_REQUEST['HumanCheck'])) {
		$response->hangup();
	}
}

Send the XML content-type header and the TwiML body.

// send response
if (!headers_sent()) {
	header('Content-type: text/xml');
}
echo $response;

Test Cases

[WHISPER-1-1] Recorded Message

Input

Parameter Value
Message https://example.com/recorded-message.mp3

Output

$ curl -s 'https://twimlets.com/whisper?Message=https%3A%2F%2Fexample.com%2Frecorded-message.mp3' \
| xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1">
    <Play>https://example.com/recorded-message.mp3</Play>
  </Gather>
</Response>

[WHISPER-1-2] Text Message

Input

Parameter Value
Message Text message

Output

$ curl -s 'https://twimlets.com/whisper?Message=Text%20message' \
| xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1">
    <Say>Text message</Say>
  </Gather>
</Response>

[WHISPER-1-3] Default Message

Input

Parameter Value
Message (omitted or empty)
From +1-916-555-0123

Output

$ curl -s 'https://twimlets.com/whisper?Message=&From=%2B19165550123' \
| xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1">
    <Say>You are receiving a call from +. 1. 9. 1. 6. 5. 5. 5. 0. 1. 2. 3. .  Press any key to accept.</Say>
  </Gather>
</Response>

[WHISPER-1-4] Human Check

Input

Parameter Value
Message Text message
HumanCheck true

Output

$ curl -s 'https://twimlets.com/whisper?Message=Text%20message&HumanCheck=true' \
| xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1">
    <Say>Text message</Say>
  </Gather>
  <Hangup/>
</Response>

[WHISPER-2-1] A Digit was Pressed

Input

Parameter Value
Digits 5

Output

$ curl -s 'https://twimlets.com/whisper?Digits=5' \
| xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<Response/>

[WHISPER-2-2] No Digits were Pressed

Input

Parameter Value
Digits (empty)

Output

$ curl -s 'https://twimlets.com/whisper?Digits=' \
| xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Hangup/>
</Response>

Unit Tests

 PASS  funlet-whisper/funlet-whisper.test.js
  ✓ [WHISPER-INPUT-MESSAGE-1] Read Message from Event (3ms)
  ✓ [WHISPER-INPUT-MESSAGE-2] Read Message from Environment
  ✓ [WHISPER-INPUT-MESSAGE-3] Read Default Message from Script (with number in From parameter) (1ms)
  ✓ [WHISPER-INPUT-MESSAGE-4] Read Default Message from Script (with number in Caller parameter)
  ✓ [WHISPER-INPUT-LANGUAGE-1] Read Language from Event
  ✓ [WHISPER-INPUT-LANGUAGE-2] Read Language from Environment
  ✓ [WHISPER-INPUT-LANGUAGE-3] Read Default Language from Script (1ms)
  ✓ [WHISPER-INPUT-VOICE-1] Read Voice from Event
  ✓ [WHISPER-INPUT-VOICE-2] Read Voice from Environment
  ✓ [WHISPER-INPUT-VOICE-3] Read Default Voice from Script
  ✓ [WHISPER-INPUT-HUMAN-CHECK-0] Read Human Check "1" from Event (1ms)
  ✓ [WHISPER-INPUT-HUMAN-CHECK-1] Read Human Check from Event
  ✓ [WHISPER-INPUT-HUMAN-CHECK-2] Read Human Check from Environment
  ✓ [WHISPER-INPUT-HUMAN-CHECK-3] Read Default Human Check from Script (1ms)
  ✓ [WHISPER-INPUT-DIGITS-0] Read No Digits from Event
  ✓ [WHISPER-INPUT-DIGITS-1] Read Empty Digits from Event
  ✓ [WHISPER-INPUT-DIGITS-2] Read Non-Empty Digits from Event
  ✓ [WHISPER-OUTPUT-SPELL-1] Spell a phone number digit by digit
  ✓ [WHISPER-OUTPUT-WHISPER-1-2] Recorded Message (3ms)
  ✓ [WHISPER-OUTPUT-WHISPER-1-4] Human Check (1ms)
  ✓ [WHISPER-OUTPUT-WHISPER-2-0] No Digits Provided
  ✓ [WHISPER-OUTPUT-WHISPER-2-1] A Digit was Pressed
  ✓ [WHISPER-OUTPUT-WHISPER-2-2] No Digits were Pressed, Empty Digits Set (1ms)
  ✓ [WHISPER-1-1] Full Response: Recorded Message
  ✓ [WHISPER-1-4] Full Response: Human Check (1ms)
  ✓ [WHISPER-2-1] Full Response: A Digit was Pressed (2ms)

Integration Tests

With https://$DOMAIN.twil.io/whisper the URL of a copy of the Whisper Funlet deployed in my Twilio account with checks for signed requests disabled, we get:

$ ./test-whisper.sh "https://$DOMAIN.twil.io/whisper"
[WHISPER-1-1] Recorded Message
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1">
    <Play>https://example.com/recorded-message.mp3</Play>
  </Gather>
</Response>

[WHISPER-1-2] Text Message
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1">
    <Say language="en" voice="alice">Text message</Say>
  </Gather>
</Response>

[WHISPER-1-3] Default Message
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1">
    <Say language="en" voice="alice">You are receiving a call from +. 1. 9. 1. 6. 5. 5. 5. 0. 1. 2. 3. . Press any key to accept.</Say>
  </Gather>
</Response>

[WHISPER-1-4] Human Check
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Gather numDigits="1">
    <Say language="en" voice="alice">Text message</Say>
  </Gather>
  <Hangup/>
</Response>

[WHISPER-2-1] A Digit was Pressed
<?xml version="1.0" encoding="UTF-8"?>
<Response/>

[WHISPER-2-2] No Digits were Pressed
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Hangup/>
</Response>

And the limited differences with the Twimlet output look alright:

$ ./test-whisper.sh "https://$DOMAIN.twil.io/whisper" | diff twimlet-whisper.log -
13c13
<     <Say>Text message</Say>
---
>     <Say language="en" voice="alice">Text message</Say>
21c21
<     <Say>You are receiving a call from +. 1. 9. 1. 6. 5. 5. 5. 0. 1. 2. 3. .  Press any key to accept.</Say>
---
>     <Say language="en" voice="alice">You are receiving a call from +. 1. 9. 1. 6. 5. 5. 5. 0. 1. 2. 3. . Press any key to accept.</Say>
29c29
<     <Say>Text message</Say>
---
>     <Say language="en" voice="alice">Text message</Say>

Closed by #40