Description: |
This is a VXML/TellMe.com book looker-upper. I have an innate ability to walk into a book, CD or DVD store, and completely forget every book, CD and DVD I own. I thought it would be cool to put them in a database, and be able to call into to TellMe with my cellphone, and find out if I already own it or not. So I wrote this. It only does books at the moment, but someday (real soon now) it'll handle the CDs and DVDs, too. It's a good simple demonstration of using VXML, CGI, and DBI.
To give it a try, call 1-800-555-TELL (8355), at the main menu say 'Extensions' or press 1. At the extensions menu, say 'five four five two nine' or press 5-4-5-2-9 (the letters spell K4JCW, my amateur callsign). The program should be able to step you through the rest. A known ISBN you can try is '0345260716' (Have Space Suit, Will Travel)
Like all Twitching Monk Software, there's a limited warranty. It's limited to assuring you that it won't get a cold beer from the 'fridge without your help, and if you drink too much, you'll fall down.
If you don't like the way I wrote something, fix it. I wrote it, I like it, I use it, it works for, see figure one. |
#!/usr/local/bin/perl -w
#
# This is an example of writing a VXML application for TellMe.com in
+Perl.
# I wrote this because when I walk into a book, CD or DVD store, I
# immediately forget what I own. Using my cell phone, I can call int
+o
# 1-800-555-TELL, select the book database, enter the ISBN number, an
+d know
# if I own it or not. I've built in some infra-structure for the CD
+and
# DVD portion, but at the moment it only returns a 'Not implemented
# message'. I'll prolly add this as I go on.
#
# To use it, you'll first need an account at www.TellMe.com. It's fr
+ee, so
# you should go get one now. As far as setting up the TellMe side, i
+t's
# pretty straight forward, and I don't want to explain it here, since
+ they
# may change it. Next, put the script in your favorite directory. I
+ put
# mine in /vxml off my docroot, and name it according to the extensio
+n it
# will be assigned to from TellMe.com. Since I have multiple extensi
+ons, it
# makes it easy to keep track of what scripts are associated with wha
+t
# extensions.
#
# You'll need to create a database (I use mySQL) with the structure t
+hat's
# at the __END__ section. It's a direct mysqldump of my Media datab
+ase.
# Set the variables below to the host, database name, etc. Security
+says
# you should make sure that the user you allow to access the database
+ only
# has SELECT privledges. If someone manages to dump your script sour
+ce, you
# don't want a DB writable password being exposed. Don't forget to p
+ut a
# index.html file in the directory also, so it's not browsable.
#
# Odds & ends: Some ISBN numbers have an 'X' in the 10th position. S
+ince a
# DTMF pad doesn't have 'X', we use '9'. If the script sees a '9' in
+ the
# 10th position it will check for both an entry ending in '9' and in
+'X'.
# If it finds both (and you'd be way out beyond the statistical norm
+if you
# did), it will read back both titles. The help could no doubt be cl
+earer,
# but since only trained users are using it, you get what I needed as
+ a
# reminder.
#
# Requires the following modules:
# CGI
# DBI
#
# Copyright 2001(c) J.C.Wren jcwren@jcwren.com
# A production of Twitching Monk Software
# No rights reserved, use as you see fit. I'd like to know about it,
+ though, just for kicks.
#
# 2001/03/23 - 1.00.00 - Initial release
#
#
use strict;
use CGI;
use DBI;
use Carp;
#
# Our script name. TellMe doesn't handle not having a script name cor
+rectly.
#
use constant cScriptName => '54529.pl';
#
# Tune these for your database setup
#
use constant cDB_Host => 'localhost'; # Host for database
use constant cDB_Database => 'Media'; # Database to use
use constant cDB_Table_Books => 'Books'; # Table name
use constant cDB_User => 'media'; # User name to access
+DB with
use constant cDB_Pass => 'media'; # Our super-secret pas
+sword (!)
#
# Action dispatch list
#
my %actionList = ('mainmenu' => \&function_mainmenu,
'books' => \&function_books,
'cds' => \&function_cds,
'dvds' => \&function_dvds,
'help' => \&function_help,
);
my %actionListBooks = ('getisbn' => \&function_books_getisbn,
'lookup' => \&function_books_lookup,
'mainmenu' => \&function_books_mainmenu,
);
#
# Help for the clueless (or me, after two days of not using it)
#
my %helpList = ('mainmenu' => qq{Say books or press 1 for the book dat
+abase.
Say C dees or press 2 for the C D dat
+abase.
Say D V dees or press 3 for the D V D
+ database.
Hang up to exit system.
End of help.
},
'getisbn' => qq{Say or type the 10 digit I S B N numb
+er.
If the number has the letter X in it
+use 9, instead.
Say menu or press star to return to t
+he main menu, or hang up.
End of help.
},
'unknown' => qq{Unknown help request.
},
);
#
# Main
#
{
my $cgi = new CGI;
$| = 1;
$cgi->cache (1);
my $goto = $cgi->param ('goto') || 'mainmenu';
$goto = 'mainmenu' if (!$actionList {$goto});
&{$actionList {$goto}} ($cgi);
}
#
# Main menu (doncha love redundant comments?)
#
sub function_mainmenu
{
my $cgi = shift;
write_headers ($cgi);
print <<"ENDHERE";
<vxml>
<form id="function_mainmenu">
<field name="function">
<grammar>
<![CDATA[
[
[dtmf-1 books] {<option "books">}
[dtmf-2 seedees] {<option "cds">}
[dtmf-3 deeveedees] {<option "dvds">}
]
]]>
</grammar>
<prompt>
<audio>
Select option, or say help.
</audio>
</prompt>
<nomatch>
<audio>
I'm sorry, I didn't understand what you said.
</audio>
<reprompt/>
</nomatch>
<help>
<goto next="@{[cScriptName]}?goto=help&helplevel=mainme
+nu&lastgoto=mainmenu"/>
</help>
<filled>
<result name="books">
<goto next="@{[cScriptName]}?goto=books"/>
</result>
<result name="cds">
<goto next="@{[cScriptName]}?goto=cds"/>
</result>
<result name="dvds">
<goto next="@{[cScriptName]}?goto=dvds"/>
</result>
<audio>
I'm sorry, I didn't understand what you said.
</audio>
<reprompt/>
</filled>
<error>
<audio>
I'm sorry, I didn't understand what you said.
</audio>
<reprompt/>
</error>
</field>
</form>
</vxml>
ENDHERE
}
#
# Book handler
#
sub function_books
{
my $cgi = shift;
my $option = $cgi->param ('option') || 'getisbn';
$option = 'getisbn' if (!$actionListBooks {$option});
&{$actionListBooks {$option}} ($cgi);
}
sub function_books_getisbn
{
my $cgi = shift;
write_headers ($cgi);
print <<"ENDHERE";
<vxml>
<form id="function_books_getisbn">
<field name="isbn">
<grammar>
<![CDATA[
[
[Ten_digits]
[dtmf-star menu] {<option "mainmenu">}
]
]]>
</grammar>
<prompt>
<audio>
Say or enter the I S B N number.
Use 9 for X, and don't use dashes.
Use star or say menu to return to the main menu.
</audio>
</prompt>
<help>
<goto next="@{[cScriptName]}?goto=help&helplevel=getisb
+n&lastgoto=books&lastoption=getisbn"/>
</help>
<filled>
<result name="mainmenu">
<submit next="@{[cScriptName]}?goto=mainmenu"/>
</result>
<submit next="@{[cScriptName]}?goto=books&option=lookup
+" namelist="isbn"/>
</filled>
<noinput>
<audio>
I'm sorry, I didn't hear you.
</audio>
<pause>500</pause>
<reprompt/>
</noinput>
<error>
<audio>
I'm sorry, I didn't understand what you said.
</audio>
<pause>500</pause>
<reprompt/>
</error>
</field>
</form>
</vxml>
ENDHERE
}
sub function_books_lookup
{
my $cgi = shift;
my $isbn = $cgi->param ('isbn') || 0;
if (length ($isbn) != 10)
{
short_message ($cgi, "The I S B N entered was not 10 digits. Pl
+ease try again.", 'books', 'getisbn');
}
elsif ($isbn =~ m/[^0-9]/)
{
short_message ($cgi, "The I S B N must be all digits. Please tr
+y again.", 'books', 'getisbn');
}
else
{
if (my $database = DBI->connect ("DBI:mysql:@{[cDB_Database]}:@{
+[cDB_Host]}", cDB_User, cDB_Pass))
{
my $query = qq{SELECT Title,ISBN FROM @{[cDB_Table_Books]} WH
+ERE ISBN='$isbn'};
$query .= qq{ OR ISBN='} . substr ($isbn, 0, 9) . qq{X'} if (
+substr ($isbn, -1, 1) eq '9');
my $cursor = $database->prepare ($query) or internal_error ($
+cgi, $database->errstr);
$cursor->execute or internal_error ($cgi, $database->errstr);
my $rows = $cursor->fetchall_arrayref;
if (scalar @$rows)
{
if (scalar @$rows == 1)
{
short_message ($cgi, "I S B N present. Title is @$rows
+[0]->[0].", 'books', 'getisbn');
}
else
{
my $titles = "Multiple I S B N matches. Titles are ";
$titles .= $_->[0] . '</audio><pause>1000</pause><audio
+>' foreach (@$rows);
short_message ($cgi, $titles, 'books', 'getisbn');
}
}
else
{
short_message ($cgi, "The requested I S B N is not in the
+database. $query", 'books', 'getisbn');
}
$database->disconnect;
}
else
{
internal_error ($cgi);
}
}
}
sub function_books_mainmenu
{
my $cgi = shift;
write_headers ($cgi);
print <<"ENDHERE";
<vxml>
<form id="mainmenu">
<block>
<goto next="@{[cScriptName]}?option=mainmenu"/>
</block>
</form>
</vxml>
ENDHERE
}
#
# Had I written something to handle CDs, it would go here.
#
sub function_cds
{
function_not_implemented (shift);
}
#
# Had I written something to have DVDs, it would go here.
#
sub function_dvds
{
function_not_implemented (shift);
}
#
# Help dispatcher.
#
sub function_help
{
my $cgi = shift;
my $help = $cgi->param ('helplevel') || 'unknown';
my $goto = $cgi->param ('lastgoto') || 'mainmenu';
my $option = $cgi->param ('lastoption') || undef;
short_message ($cgi, $helpList {$help}, $goto, $option);
}
#
# Internal errors are *bad*. Just tell the user, and hang up.
# It's not like this is a life support application, or a pr0n
# credit card service where error recovery is a must...
#
sub internal_error
{
my ($cgi, $msg) = @_;
write_headers ($cgi);
print <<"ENDHERE";
<vxml>
<form id="error_internal">
<block>
<audio>
An internal error has occurred. Please try again later.
</audio>
<pause>500</pause>
<disconnect/>
</block>
</form>
<!-- $msg -->
</vxml>
ENDHERE
die $msg;
}
#
# For the parts I've been too fscking lazy to write
#
sub function_not_implemented
{
short_message (shift, "Sorry, that function isn't implemented yet."
+, 'mainmenu');
}
#
# Handle like short message routine.
#
sub short_message
{
my ($cgi, $msg, $goto, $option) = @_;
my $parms = "";
$parms = "?" if defined ($goto) || defined ($option);
$parms .= "goto=$goto" if defined ($goto);
$parms .= "&option=$option" if defined ($option);
write_headers ($cgi);
print <<"ENDHERE";
<vxml>
<form id="error">
<block>
<audio>
$msg
</audio>
<pause>500</pause>
<goto next="@{[cScriptName]}$parms"/>
</block>
</form>
</vxml>
ENDHERE
}
#
# Generic header for TellMe. No cacheing *very* important. TellMe
# does bad things if you don't tell him you're not cacheable.
#
sub write_headers
{
my $cgi = shift;
print $cgi->header (-expires => 'Mon, 26 Jul 1997 05:00:00 GMT',
-last_modified => scalar localtime (),
-cache_control => 'no-cache, must-revalidate',
-pragma => 'no-cache');
print '<?xml version="1.0"?>', "\n";
print '<!DOCTYPE vxml PUBLIC "-//Tellme Networks//Voice Markup Lang
+uage 1.0//EN" "http://resources.tellme.com/toolbox/vxml-tellme.dtd">'
+, "\n";
}
__END__
# MySQL dump 8.12
#
# Host: localhost Database: Media
#--------------------------------------------------------
# Server version 3.23.32
#
# Table structure for table 'Books'
#
CREATE TABLE Books (
Title varchar(255) default NULL,
ISBN varchar(10) NOT NULL default '0',
PRIMARY KEY (ISBN),
UNIQUE KEY ISBN(ISBN)
) TYPE=MyISAM;
|