Voicemail Lesson 3

We now know how to play and record messages. Let's introduce a few more interesting techniques, including use of the file management system to check for the existence of specific files and use of application parameters to determine the voicemail box that the application is accessing.


    • Sample Files:
      • Samples\voicemail_3.py

      Specifying a Voicemail Box

      This step makes use of the application_parameters argument that contains the user-defined string originating from the Inbound Service configuration. This string is expected to contain the name we have set up to be our voicemail box. We set up a format for all voicemail filenames that locates them in a voicemailbox subfolder on the cloud.

      After answering the call the application identifies the level of access for the caller. It does this by comparing the caller Id with the voicemail box name, as determined from application_parameters.

      If the caller Id and voicemail box name match then the caller is deemed to be the owner of the voicemail box and the application calls top_level_menu() that allows the caller to listen and delete all messages. In the main function, we can do this using the file_man.exists() function, with the filename as a parameter. This will test for the existence of the file on the Cloud. We keep incrementing a counter until the filename is not found.

      If the caller Id and voicemail box name do not match, the caller is simply allowed to record a new message.

      Using the channel

      One of the first things we do is to call our get_caller function. This extacts information from the call_from property of the channel we've been given, which supplies a SIP address. Our telephony system which answers the client encapsulates the phone number information within a SIP format, which is then used by our Cloud. Our function extracts the phone number information from the SIP address.

      The application parameters are given to us as a string, accessable by the application_parameters variable. In our example, we compare the parameter against the caller number, to identify if the caller is actually the owner of our voicemail service. If they are the caller, the application jumps to the top_level_menu subroutine, otherwise to record_new_message.

      Dealing with DTMF

      The top level menu subroutine uses a function called get_menu_select. We saw in the previous example how to access DTMF tones via the keypad. This routine will play some TTS to the user, and then wait for a valid response. Once the user has entered a valid response, the keypad press will be returned. We can then simply check the return code and act appropriately.

      In play_all_messages(), we use a counter, checking if a filename derived from the counter exists, and if so, says some TTS and then the file itself. delete_all_messages() works in a similar manner, but rather than play each file, we delete the file using the file_man.delete_file call.

    • __uas_identify__ = "application"
      __uas_version__ = "1.0b1"
      
      from prosody.uas import Hangup, Error, ICanRun
      
      """
      
          An inbound application that answers the call and says
          "Welcome to the voicemail server for <name>". <name> is obtained from the
          application_parameters and must be registered on the inbound services
          page of the CWP.
      
          The application uses <name> to determine whether the caller owns the
          voicemail box. It does this by comparing <name> with the <call_from>
          property of the inbound call.
      
          If the caller owns the voicemail box, the top level options given are:
      
              1. Play all existing messages
              2. Delete all existing messages
      
          If the caller does not own the voicemail box, he can leave a new
          message. The name of the WAV file to save to will be
          "samples/voicemail3/<name>/msg<counter>.wav". The <counter> is a
          variable, initialised to 1, which increments each time a message is
          successfully saved.
      
          This application is part of the online Voice Mail tutorial.
      """
      
      class VoiceMailBox:
          def __init__(self, box_name, channel, file_manager, logger):
              self._channel = channel
              self._file_manager = file_manager
              self._logger = logger
              self._file_format = "samples/voicemail3/" + box_name + "/msg{0}.wav"
              self._recorded_file_counter = 1
              self._set_message_counter()
      
      
          def _set_message_counter(self):
              # check the message box and set the counter
              while(self._file_manager.exists(self._file_format.format(self._recorded_file_counter))):
                  self._logger.info("{0} exists".format(self._recorded_file_counter))
                  self._recorded_file_counter += 1
      
      
          def say_tts(self, message, bargein=False, beep=False):
              cause = self._channel.FilePlayer.say(message, barge_in=bargein)
              if cause == self._channel.FilePlayer.Cause.BARGEIN:
                  return
              if cause == self._channel.FilePlayer.Cause.NORMAL:
                  if beep is True:
                      cause = self._channel.DTMFPlayer.play("#");
                      if cause != self._channel.DTMFPlayer.Cause.NORMAL:
                          raise Error("Play tone failed: caused is {0}".format(cause))
              else:
                  raise Error("Say '{0}' failed: cause is {1}".format(message, cause))
      
      
          def play_file(self, filename):
              cause = self._channel.FilePlayer.play(filename)
              if cause != self._channel.FilePlayer.Cause.NORMAL:
                  raise Error("Play '{0}' failed: cause is {1}".format(filename, cause))
      
      
          def record_new_message(self, replace=False):
      
              if replace is True and self._recorded_file_counter > 1:
                  self._recorded_file_counter -= 1
      
              filename = self._file_format.format(self._recorded_file_counter)
              self._recorded_file_counter += 1
      
              self.say_tts("Please record a message after the tone. Press any digit to stop the recording.", beep=True)
      
              cause = self._channel.FileRecorder.record(filename, milliseconds_max_silence=3000,
                                                        seconds_timeout=60, barge_in=True)
              if cause != self._channel.FileRecorder.Cause.SILENCE and cause != self._channel.FileRecorder.Cause.BARGEIN:
                  raise Error("Record message failed: cause is {0}".format(cause))
              # check whether the file has been saved correctly, does it exist?
              if not self._file_manager.exists(filename):
                  self.say_tts("Your message could not be saved.")
              else:
                  self.say_tts("The following message has been recorded")
                  self.play_file(filename)
      
      
          def play_all_messages(self):
              counter = 1
              filename = self._file_format.format(counter)
      
              while self._file_manager.exists(filename):
                  self.say_tts("Message {0}".format(counter))
                  self.play_file(filename)
                  counter += 1
                  filename = self._file_format.format(counter)
      
              if counter == 1:
                  self.say_tts("You have no messages to play.")
              else:
                  self.say_tts("End of messages.")
      
      
          def delete_all_messages(self):
              counter = 1
              filename = self._file_format.format(counter)
      
              while self._file_manager.exists(filename):
                  self._file_manager.delete_file(filename)
                  counter += 1
                  filename = self._file_format.format(counter)
      
              if counter == 1:
                  self.say_tts("You have no messages to delete.")
              else:
                  self.say_tts("All messages deleted.")
      
              self._recorded_file_counter = 1
      
      
      def main(channel, application_instance_id, file_man, my_log, application_parameters):
          my_log.info ("Voicemail 3 started.")
          return_code = 0
          try:
      
              # The voicemail box name must have been configured at service registration
              if len(application_parameters) == 0:
                  raise Error("Argument list is empty")
      
              mailbox = VoiceMailBox(application_parameters, channel, file_man, my_log)
      
              channel.ring(2)
              channel.answer()
              caller = get_caller(channel)
      
              my_log.info ("Call from {0} to {1}'s Voicemail answered".format(caller, application_parameters))
      
              mailbox.say_tts("Welcome to the voicemail server for {0}".format(" ".join(list(application_parameters))))
      
              if caller == application_parameters:
                  top_level_menu(channel, mailbox)
              else:
                  replace = False
                  # wait on i_can_run to prevent an accidental infinite loop
                  i_can_run = ICanRun(channel)
                  while i_can_run:
                      mailbox.record_new_message(replace)
                      key_selected = get_menu_select(channel, mailbox,
                                                     "Please press 1 to keep this message or 2 to record an alternative message",
                                                     20, "12")
      
                      if key_selected == '1':
                          mailbox.say_tts("Message saved.")
                          break
                      replace = True
      
              mailbox.say_tts("Goodbye.")
      
              channel.hang_up();
      
          except Hangup as exc:
              my_log.info("Completed with Hangup: {0}".format(exc))
              return_code = -100
      
          except Error as exc:
              my_log.error("Completed with Error exception! {0}".format(exc))
              return_code = -101
      
          except Exception as exc:
              my_log.exception("Completed with exception! {0}".format(exc))
              return_code = -102
      
          finally:
              if channel.state() != channel.State.IDLE:
                  channel.hang_up()
          my_log.info("Voicemail 3 example completed")
          return return_code
      
      
      
      def get_caller(channel):
          caller = channel.Details.call_from
          # Extract the username from the SIP address
          if caller.startswith("sip:"):
              caller = caller[4:]
          return caller.split('@', 1)[0]
      
      
      def top_level_menu(channel, mailbox):
      
          key_selected = get_menu_select(channel, mailbox,
                                        "Press 1 to listen to all messages. \
                                        Press 2 to delete all messages. \
                                        Press 3 to quit.",
                                        20, "123")
          if key_selected == '1':
              mailbox.play_all_messages()
          elif key_selected == '2':
              mailbox.delete_all_messages()
      
      
      def get_menu_select(channel, mailbox, prompt, seconds_repeat_prompt, valid_keys):
          channel.DTMFDetector.clear_digits()
      
          # wait on i_can_run to prevent an accidental infinite loop
          i_can_run = ICanRun(channel)
          while i_can_run:
              mailbox.say_tts(prompt, bargein=True, beep=True)
      
              digits = channel.DTMFDetector.get_digits(end=valid_keys, seconds_predigits_timeout=seconds_repeat_prompt)
              cause = channel.DTMFDetector.cause()
              if cause == channel.DTMFDetector.Cause.END:
                  return digits[-1]
              else:
                  mailbox.say_tts("Invalid selection.")
                                  
    • Sample Files:
      • Samples\UASApplications\Voicemail3\src\main\java\com\aculab\uas\app\Voicemail3.java

      Specifying a Voicemail Box

      This step makes use of the applicationArguments argument that contains the user-defined string originating from the Inbound Service configuration. This string is expected to contain the name we have set up to be our voicemail box. We set up a format for all voicemail filenames that locates them in a voicemailbox subfolder on the cloud. Additionally the message names are now stored in a database (REQUIRES DATABASE SETUP: see Database Tutorial for instructions).

      After answering the call the application identifies the level of access for the caller. It does this by comparing the caller Id with the voicemailBox name, as determined from applicationArguments. For SIP calls, the voicemailBox name is matched with the username of the caller (the string before the '@' character) while for PSTN calls the caller Id is simply the calling number.

      If the caller Id and voicemailBox name match then the caller is deemed to be the owner of the voicemail box and the application calls topLevelMenu() that allows the caller to listen and delete all messages. Otherwise the caller is simply allowed to record a new message.

      Dealing with DTMF

      For menu entry via the keypad, a new method has been created called getMenuSelection(). This reads out a prompt, passed as a parameter, a predigit timeout value, and also any valid selections that the user is able to press. A string of the single digit reply is returned. In the topLevelMenu() method, pressing a '1' will call the playAllMessages() method and pressing a '2' will call deleteAllMessage().

      Database

      The names of the message files have been abstracted and stored in a local database (class VoicemailBoxDatabase). As the cloud file system stores the messages files in a reliable and distributed manner, there can be some delays before files are available to read. Using the database ensures the list of files the sample manipulates is accurate and up to date.

      Voicemail functionality

      In playAllMessages() all the recorded messages maintained in the database are played if they are available on the cloud. deleteAllMessage() does a similar job, testing for the existence of a derived filename, but rather than play it, will delete the file with the getFileManager().deleteFile() method.

      The recordNewMessage() method behaves like the record method in the previous step, except that it now uses the getMenuSelection method to determine user input.

    • package com.aculab.uas.app;
      
      // An inbound application that answers the call and compares the <callFrom>
      // property of the inbound call with the voicemailbox name
      // specified in applicationParameters.
      //
      // that defaults to "Welcome to the voicemail server for <name>." but can
      // be changed. <name> is obtained from the applicationArguments.
      //
      // If <callFrom> equals <voicemailbox>
      // Say an introduction message and then
      //     Say "Press 1 to listen to all messages.
      //   Press 2 to delete all messages.
      //     1. Play all existing messages - with option of deleting each one individually.
      //     2. Delete all existing messages
      //   All options return to the above menu.
      // else
      //     Do voicemail message recording as in voicemail 2.
      //
      // The name of the .wav file to save to will be "samples/voicemail4/voicemailbox/msg_<guid>.wav"
      // where guid is a unique identifier.
      //
      // This application uses a MYSQL database to manage the recorded message files.
      //
      // Requires:
      // [applicationParameters = voicemailbox name]
      import java.sql.Connection;
      import java.sql.DriverManager;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.UUID;
      
      import com.aculab.amsapi.*;
      import com.aculab.uasapi.*;
      
      // The application class.
      // This must extend either UASInboundApplication or UASOutboundApplication and
      // override the run method.
      // It must be packaged in a jar file of the same name together with a manifest file.
      public class Voicemail3 extends UASInboundApplication
      {
          // Success Codes:
          final int Success = 0;
          final int RecordedAMessage = 1;
          final int CallHungUp = 2;
          // ... any positive integer
      
          // Fail Codes:
          // -1 to -99 reserved
          final int ExceptionThrown = -100;
          final int ArgumentEmpty = -101;
          final int RecordingFailedToStart = -102;
      
          // This is the entry point for the application
          @Override
          public int run(UASCallChannel channel, String applicationParameters)
               throws Exception
           {
              this.getTrace().traceInfo("Start - appParms [%s]", applicationParameters);
              int returnCode = Success;
      
              // The voicemail box name must have been configured in the
              // applicationParameters at service registration
              _voicemailBoxName = applicationParameters;
              if (_voicemailBoxName.length() == 0)
              {
                  this.getTrace().traceInfo("The required voicemailbox name must be supplied in application parameters");
                   return ArgumentEmpty;
              }
      
              try
              {
                  // Set up the location for the recorded message files
                  _messageFolder = "samples/voicemail4/" + _voicemailBoxName + "/";
      
                  // Setup the location and format for the recorded filenames
                  _messageFileNameFormat = "msg_%s.wav";
      
                  // Answer the call
                  CallState state = channel.answer();
                  if (state == CallState.Answered)
                  {
                      // Create Database Connection Object
                      _voicemailBoxDatabase = new VoicemailBoxDatabase("test", "user", "password", applicationParameters);
      
                      // Open the database
                      _voicemailBoxDatabase.open();
      
                      String caller = getCaller(channel);
                      this.getTrace().traceInfo("Call from %s to %s's Voicemail answered",
                                                 caller, _voicemailBoxName);
      
                      // If the caller is the owner of the voicemailbox let them manage their voicemailbox
                      // Else record a message
                      if (caller.compareTo(_voicemailBoxName) == 0)
                      {
                          topLevelMenu(channel);
                      }
                      else
                      {
                          playWelcomeMessage(channel);
      
                          recordNewMessage(channel);
      
                          channel.getFilePlayer().say("Goodbye.");
                      }
                  }
              }
              catch (Exception e)
              {
                  this.getTrace().traceError("Exception thrown %s", e.getMessage());
                  returnCode = ExceptionThrown;
              }
              finally
              {
                  channel.hangUp();
                  _voicemailBoxDatabase.close();
              }
      
              this.getTrace().traceInfo("Completed with return code %d", returnCode);
              return returnCode;
           }
      
          // Play the recorded welcome message or a standard welcome message if it
          // doesn't exist.
          private int playWelcomeMessage(UASCallChannel channel)
                  throws IllegalArgumentException, IllegalStateException, InterruptedException
          {
              channel.getFilePlayer().say("Welcome to the voicemail server for %s", _voicemailBoxName);
              return Success;
          }
      
          // Obtain the caller id
          private String getCaller(UASCallChannel channel)
                  throws ObjectDisposedException
          {
              // Extract the user part of a SIP address
              String caller = channel.getCallDetails().getCallFrom();
              if (caller.contains("sip:"))
              {
                  caller = caller.substring(4);
                  int pos = caller.indexOf('@');
                  if (pos >= 0)
                  {
                      caller = caller.substring(0, pos);
                  }
              }
              return caller;
          }
      
          // Get a new message filename
          private String getNextRecordFileName()
          {
              return String.format(_messageFileNameFormat, UUID.randomUUID().toString());
          }
      
          // Run the top level menu for the voicemailbox owner.
          private int topLevelMenu(UASCallChannel channel)
                  throws IllegalStateException, InterruptedException, IllegalArgumentException, ObjectDisposedException, SQLException
          {
              channel.getFilePlayer().say("This is the voice mailbox for %s", _voicemailBoxName);
      
              int reply = Success;
              String prompt = "Press 1 to listen to all messages. " +
                              "Press 2 to delete all messages. " +
                              "Press hash to exit.";
              char keySelected;
              do
              {
                  keySelected = getMenuSelection(channel, prompt, 20, "12#");
                  if (keySelected == (char)-1)
                  {
                      reply = CallHungUp;
                  }
                  else
                  {
                      switch (keySelected)
                      {
                          case '1':
                              reply = playAllMessages(channel);
                              break;
                          case '2':
                              deleteAllMessages(channel);
                              break;
                          case '#':
                              channel.getFilePlayer().say("Goodbye");
                              return reply;
                      }
                  }
              } while (reply == Success);
              return reply;
          }
      
          private int recordNewMessage(UASCallChannel channel)
                  throws IllegalStateException, InterruptedException, IllegalArgumentException, ObjectDisposedException, SQLException
          {
              int reply = Success;
              String fileName = getNextRecordFileName();
              String prompt = "Please record a message after the tone. " +
                           "Press any digit to stop the recording.";
      
              String fullFileName = _messageFolder + fileName;
      
              // Loop round until we have recorded a message acceptable to the caller
              // or caller has hung up
              boolean satisfiedWithMessage = false;
              do
              {
                  // Play the prompt
                  channel.getFilePlayer().say(prompt);
      
                  // Delay before starting the recording and playing the tone (for effect)
                  Thread.sleep(1000);
      
                  // Clear any received digits
                  channel.getDtmfDetector().clearDigits();
      
                  // Start recording, enabling barge in
                  this.getTrace().traceInfo("Recording to file: [%s]", fullFileName);
                  if (!channel.getFileRecorder().start(fullFileName, true))
                  {
                      this.getTrace().traceError("Recording failed to start cause = %s",
                              channel.getFileRecorder().getCause());
                      reply = RecordingFailedToStart;
                      break;
                  }
      
                  // Play a tone to signal that recording has started
                  channel.getDtmfPlayer().play("0");
      
                  // Wait for a period for recording to complete or
                  // the call to have been hung up
                  FileRecorderCause recCause = channel.getFileRecorder().waitUntilFinished(60);
                  if (recCause == FileRecorderCause.HangUp)
                  {
                      this.getTrace().traceInfo("Call has been hung up");
                      reply = CallHungUp;
                      break;
                  }
                  if (recCause == FileRecorderCause.Timeout)
                  {
                      this.getTrace().traceInfo("Recording has stopped after 60 seconds");
                      channel.getFileRecorder().stop();
                  }
      
                  // Replay the recorded message
                  channel.getFilePlayer().say("The following message has been recorded");
                  if (channel.getFilePlayer().play(fullFileName, true) == FilePlayerCause.HangUp)
                  {
                      this.getTrace().traceInfo("Call has been hung up");
                      reply = CallHungUp;
                      break;
                  }
      
                  // Check if the user is satisfied with the recorded message
                  String acknowledgePrompt = "Please press 1 to keep this message or 2 to record an " +
                                             "alternative message";
                  char keySelected = getMenuSelection(channel, acknowledgePrompt, 20, "12");
                  if (keySelected == -1)
                  {
                      reply = CallHungUp;
                  }
                  else if (keySelected == '1')
                  {
                      satisfiedWithMessage = true;
                      _voicemailBoxDatabase.saveMessage(fileName);
                      channel.getFilePlayer().say("Message saved.");
                  }
              } while (!satisfiedWithMessage && (reply == Success));
      
              return reply;
          }
      
          // Play all the recorded messages, allowing the caller to delete any as required
          private int playAllMessages(UASCallChannel channel)
                  throws IllegalStateException, InterruptedException, IllegalArgumentException, ObjectDisposedException, SQLException
          {
              int reply = Success;
      
              List<String> fileNames = _voicemailBoxDatabase.getMessageFileNames();
              for (String fileName : fileNames)
              {
                  String fullFileName = _messageFolder + fileName;
      
                  if (!this.getFileManager().fileExists(fullFileName))
                  {
                      channel.getFilePlayer().say("The next message is currently inaccessable");
                      Thread.sleep(1000);
                      continue;
                  }
      
                  channel.getFilePlayer().say("Next message");
      
                  channel.getFilePlayer().play(fullFileName);
                  if (channel.getFilePlayer().getCause() == FilePlayerCause.HangUp)
                  {
                      reply = CallHungUp;
                      break;
                  }
              }
      
              if (reply == Success)
              {
                  channel.getFilePlayer().say("No more messages.");
              }
      
              return reply;
          }
      
          // Delete all recorded messages, both in the database and the files themselves
          private void deleteAllMessages(UASCallChannel channel)
                  throws IllegalStateException, InterruptedException, IllegalArgumentException, SQLException
          {
              // Get the names of all existing files
              List<String> fileNames = _voicemailBoxDatabase.getMessageFileNames();
      
              // Delete them from the db
              _voicemailBoxDatabase.deleteAllMessages();
      
              // Delete all the message files
              if (fileNames.size() > 0)
              {
                  for (String fileName : fileNames)
                  {
                      this.getFileManager().deleteFile(_messageFolder + fileName);
                  }
                  channel.getFilePlayer().say("All messages have been deleted.");
              }
              else
              {
                  channel.getFilePlayer().say("There are no messages to delete.");
              }
          }
      
          // Find out what the caller wants to do.
          // Returns: DTMF key (on of validKeys) if one was selected, -1 if call hung up.
          private char getMenuSelection(
                  UASCallChannel channel,
                  String prompt,
                  int secondsRepeatPrompt,
                  String validKeys)
                      throws
                          IllegalStateException,
                          InterruptedException,
                          IllegalArgumentException,
                          ObjectDisposedException
          {
              // Prepare to receive digits
              channel.getDtmfDetector().clearDigits();
      
              do
              {
                  FilePlayerCause cause = channel.getFilePlayer().say(true, prompt);
                  if (cause == FilePlayerCause.HangUp)
                  {
                      return (char)-1;
                  }
      
                  String digits = channel.getDtmfDetector().getDigits(1, secondsRepeatPrompt);
                  if (channel.getDtmfDetector().getCause() == DtmfDetectorCause.Count)
                  {
                      if (validKeys.contains(digits))
                      {
                          char keySelected = digits.charAt(0);
                          this.getTrace().traceInfo("Got keypress %c", keySelected);
                          return keySelected;
                      }
                      else
                      {
                          channel.getFilePlayer().say("Selection not valid.");
                      }
                  }
              } while (channel.getDtmfDetector().getCause() != DtmfDetectorCause.HangUp);
      
              // returning on hangup
              return (char)-1;
          }
      
          private String _messageFolder;
          private String _messageFileNameFormat;
          private VoicemailBoxDatabase _voicemailBoxDatabase;
          private String _voicemailBoxName;
      }
      
      // A class that represents a database of voicemail messages.
      class VoicemailBoxDatabase
      {
          public VoicemailBoxDatabase(String databaseName, String user, String password, String tableName)
          {
              // add a non-numeric prefix to the table name
              _databaseName = databaseName;
              _user = user;
              _password = password;
              _tableName = "vmb" + tableName;
              _mySqlConnection = null;
          }
      
          public void open() throws ClassNotFoundException, SQLException
          {
              String con = String.format("jdbc:mysql://localhost/%s?user=%s&password=%s",
                      _databaseName, _user, _password);
              Class.forName("com.mysql.jdbc.Driver");
              _mySqlConnection = DriverManager.getConnection(con);
      
              String command = "use " + _databaseName;
              executeSqlNonQueryCommand(command);
      
              // Create the table if it doesn't already exist.
              createTable();
          }
      
          public void close() throws SQLException
          {
              _mySqlConnection.close();
          }
      
          public void saveMessage(String fileName) throws SQLException
          {
              // Save an entry in the database
              String command = "insert into " + _tableName;
              command += " values (NOW(), '" + fileName + "')";
              executeSqlNonQueryCommand(command);
          }
      
          public void deleteMessage(String fileName) throws SQLException
          {
              // Delete the entry in the database
              String command = "delete from " + _tableName;
              command += " where filename='" + fileName + "'";
              executeSqlNonQueryCommand(command);
          }
      
          public void deleteAllMessages() throws SQLException
          {
              // Delete all the entries in the database
              String command = "delete from " + _tableName;
              executeSqlNonQueryCommand(command);
          }
      
          public List<String> getMessageFileNames() throws SQLException
          {
              List<String> fileNames = new ArrayList<String>();
              String command = "select * from " + _tableName;
              PreparedStatement myCommand = _mySqlConnection.prepareStatement(command);
              ResultSet results = myCommand.executeQuery();
      
              // Read the row data
              while (results.next())
              {
                  String fileName = results.getString("filename");
                  fileNames.add(fileName);
              }
              results.close();
      
              return fileNames;
          }
      
          // Create the table if necessary
          private void createTable() throws SQLException
          {
              try
              {
                  String command = String.format(
                          "SELECT 1 FROM %s",
                          _tableName);
                  executeSqlNonQueryCommand(command);
              }
              catch (Exception e)
              {
                  String command = String.format(
                          "CREATE TABLE %s (timestamp DATETIME, filename VARCHAR(64));",
                          _tableName);
      
                  executeSqlNonQueryCommand(command);
              }
          }
      
          private void executeSqlNonQueryCommand(String command) throws SQLException
          {
              PreparedStatement myCommand = _mySqlConnection.prepareStatement(command);
              myCommand.execute();
          }
      
          private String _databaseName;
          private String _user;
          private String _password;
          private String _tableName;
          private Connection _mySqlConnection;
      }
                                  
    • Sample Files:
      • Samples\C#\Voicemail3\Voicemail3.cs

      Specifying a Voicemail Box

      This step makes use of the applicationArguments argument that contains the user-defined string originating from the Inbound Service configuration. This string is expected to contain the name we have set up to be our voicemail box. We set up a format for all voicemail filenames that locates them in a voicemailbox subfolder on the cloud.

      After answering the call the application identifies the level of access for the caller. It does this by comparing the caller Id with the voicemailBox name, as determined from applicationArguments. For SIP calls, the voicemailBox name is matched with the Username of the caller (the string before the '@' character) while for PSTN calls the caller Id is simply the calling number.

      If the caller Id and voicemailBox name match then the caller is deemed to be the owner of the voicemail box and the application calls TopLevelMenu() that allows the caller to listen and delete all messages. Otherwise the caller is simply allowed to record a new message.

      File checking

      In the Run() method we check for the existence of a file using the FileManager.FileExists() method. We continually increment the recordedFileCounter variable until we identify a free filename - this is our next filename for recording.

      Dealing with DTMF

      For menu entry via the keypad, a new method has been created called GetMenuSelection(). This reads out a prompt, passed as a parameter, a predigit timeout value, and also any valid selections that the user is able to press. A string of the single digit reply is returned. In the TopLevelMenu() method, pressing a '1' will call the PlayAllMessages() method and pressing a '2' will call DeleteAllMessage().

      Voicemail functionality

      In PlayAllMessages(), a counter is incremented from 1, testing with a derived filename is the file exists, and if so, will play the file. This continues until there are no more messages. DeleteAllMessage() does a similar job, testing for the existence of a derived filename, but rather than play it, will delete the file with the FileManager.DeleteFile() method.

      The RecordNewMessage() method behaves like the record method in the previous step, except that it now uses the GetMenuSelection method to determine user input.

    • using System;
      using System.Threading;
      using AMSClassLibrary;
      using UASAppAPI;
      
      // An inbound application that answers the call, says "Welcome to the
      // voicemail server for <name>."
      // <name> is obtained from the applicationArguments.
      // It is compared with the <callFrom> property of the inbound call.
      //
      // If <callFrom> equals <name>
      //     Say "Press 1 to hear all messages, press 2 to delete all messages".
      //     1. Play all existing messages
      //     2. Delete all existing messages
      // else
      //     Do recording as in voicemail 2.
      //
      // All options return to the above menu.
      // The app only completes when the call is hung up.
      // The name of the .wav file to save to will be "samples/voicemail3/name/msg<counter>.wav".
      // The <counter> is a variable, initialised to 1, which increments each time a
      // message is successfully saved.
      //
      // Requires:
      // [applicationParameters = voicemail box name]
      namespace Voicemail3
      {
          public class Voicemail3 : UASInboundApplication
          {
              // Possible return codes
              enum ReturnCode
              {
                  // Success Codes:
                  Success = 0,
                  // ... any positive integer
                  RecordedAMessage = 1,
                  CallHungUp = 2,
      
                  // Fail Codes:
                  // -1 to -99 reserved
                  ExceptionThrown = -100,
                  ArgumentEmpty = -101,
                  RecordingFailedToStart = -102
              }
      
              // This is the entry point for the application
              public override int Run(UASCallChannel channel,
                                      string applicationParameters)
              {
                  this.Trace.TraceInfo("Started");
                  ReturnCode reply = ReturnCode.Success;
      
                  // The voicemail box name must have been configured at service registration
                  voicemailBoxName = applicationParameters;
                  if (voicemailBoxName.Length == 0)
                  {
                      return (int)ReturnCode.ArgumentEmpty;
                  }
      
                  recordFilenameFormat = "samples/voicemail3/" + voicemailBoxName + "/msg{0}.wav";
      
                  // Obtain the next free counter
                  recordedFileCounter = 1;
                  while (this.FileManager.FileExists(String.Format(recordFilenameFormat, recordedFileCounter.ToString())))
                  {
                      recordedFileCounter++;
                  }
      
                  try
                  {
                      // Answer the call
                      CallState state = channel.Answer();
                      if (state == CallState.Answered)
                      {
                          string caller = GetCaller(channel);
                          this.Trace.TraceInfo("Call from {0} to {1}'s Voicemail answered",
                                                      caller, voicemailBoxName);
      
                          PlayWelcomeMessage(channel, voicemailBoxName);
      
                          if (caller == voicemailBoxName)
                          {
                              TopLevelMenu(channel);
                          }
                          else
                          {
                              RecordNewMessage(channel);
                              channel.FilePlayer.Say("Goodbye.");
                          }
                      }
                  }
                  catch (Exception e)
                  {
                      this.Trace.TraceError("Exception thrown {0}", e.Message);
                      reply = ReturnCode.ExceptionThrown;
                  }
                  finally
                  {
                      channel.HangUp();
                  }
      
                  this.Trace.TraceInfo("Completed");
                  return (int)reply;
              }
      
              private ReturnCode PlayWelcomeMessage(UASCallChannel channel, string name)
              {
                  channel.FilePlayer.Say("Welcome to the voicemail server for {0}", name);
                  return ReturnCode.Success;
              }
      
              private string GetCaller(UASCallChannel channel)
              {
                  // Extract the user part of a SIP address
                  string caller = channel.CallDetails.CallFrom;
                  if (caller.Contains("sip:"))
                  {
                      caller = caller.Substring(4);
                      int pos = caller.IndexOf('@');
                      if (pos >= 0)
                      {
                          caller = caller.Remove(pos);
                      }
                  }
                  return caller;
              }
      
              private ReturnCode TopLevelMenu(UASCallChannel channel)
              {
                  ReturnCode reply = ReturnCode.Success;
                  string prompt = "Press 1 to listen to all messages. " +
                                  "Press 2 to delete all messages. " +
                                  "Press hash to exit.";
                  char keySelected;
      
                  do
                  {
                      reply = GetMenuSelection(channel, prompt, 20, "12#", out keySelected);
                      if (reply == ReturnCode.Success)
                      {
                          switch (keySelected)
                          {
                              case '1':
                                  reply = PlayAllMessages(channel);
                                  break;
                              case '2':
                                  reply = DeleteAllMessages(channel);
                                  break;
                              case '#':
                                  return reply;
                          }
                      }
                  } while (reply == ReturnCode.Success);
      
                  return reply;
              }
      
              private ReturnCode RecordNewMessage(UASCallChannel channel)
              {
                  ReturnCode reply = ReturnCode.Success;
      
                  string filename = String.Format(recordFilenameFormat, recordedFileCounter.ToString());
                  recordedFileCounter++;
      
                  // Loop round until have recorded a message acceptable to the caller
                  // or caller has hung up
                  bool satisfiedWithMessage = false;
                  do
                  {
                      // Prompt to leave a message
                      channel.FilePlayer.Say("Please record a message after the tone. " +
                                             "Press any digit to stop the recording.");
      
                      // Delay before starting the recording and playing the tone (for effect)
                      Thread.Sleep(1000);
      
                      // Clear any received digits
                      channel.DtmfDetector.ClearDigits();
      
                      // Start recording, enabling barge in
                      this.Trace.TraceInfo("Recording message to file: [{0}]", filename);
                      if (!channel.FileRecorder.Start(filename, true))
                      {
                          this.Trace.TraceError("Recording failed to start cause = {0}",
                                                channel.FileRecorder.Cause);
                          reply = ReturnCode.RecordingFailedToStart;
                          break;
                      }
      
                      // Play a tone to signal that recording has started
                      channel.DtmfPlayer.Play("0");
      
                      // Wait for a period for recording to complete or
                      // the call to have been hung up
                      FileRecorderCause recCause = channel.FileRecorder.WaitUntilFinished(60);
                      if (recCause == FileRecorderCause.HangUp)
                      {
                          this.Trace.TraceInfo("Call has been hung up");
                          reply = ReturnCode.CallHungUp;
                          break;
                      }
                      if (recCause == FileRecorderCause.Timeout)
                      {
                          this.Trace.TraceInfo("Recording has stopped after 60 seconds");
                          channel.FileRecorder.Stop();
                      }
      
                      // Replay the recorded message
                      channel.FilePlayer.Say("The following message has been recorded");
                      if (channel.FilePlayer.Play(filename, true) == FilePlayerCause.HangUp)
                      {
                          this.Trace.TraceInfo("Call has been hung up");
                          reply = ReturnCode.CallHungUp;
                          break;
                      }
      
                      // Check if the user is satisfied with the recorded message
                      string prompt = "Please press 1 to keep this message or 2 to record an " +
                                      "alternative message";
                      char keySelected;
      
                      reply = GetMenuSelection(channel, prompt, 20, "12", out keySelected);
                      if (reply == ReturnCode.Success)
                      {
                          if (keySelected == '1')
                          {
                              satisfiedWithMessage = true;
                              channel.FilePlayer.Say("Message saved.");
                          }
                      }
                  } while (!satisfiedWithMessage && (reply == ReturnCode.Success));
      
                  return reply;
              }
      
              private ReturnCode PlayAllMessages(UASCallChannel channel)
              {
                  ReturnCode reply = ReturnCode.Success;
                  int counter = 1;
      
                  string filename = String.Format(recordFilenameFormat, counter.ToString());
      
                  while (this.FileManager.FileExists(filename))
                  {
                      channel.FilePlayer.Say("Message " + counter.ToString());
      
                      channel.FilePlayer.Play(filename);
                      if (channel.FilePlayer.Cause == FilePlayerCause.HangUp)
                      {
                          reply = ReturnCode.CallHungUp;
                          break;
                      }
      
                      counter++;
                      filename = String.Format(recordFilenameFormat, counter.ToString());
                  }
      
                  if (reply == ReturnCode.Success)
                  {
                      channel.FilePlayer.Say("No more messages.");
                  }
      
                  return reply;
              }
      
              private ReturnCode DeleteAllMessages(UASCallChannel channel)
              {
                  ReturnCode reply = ReturnCode.Success;
                  int counter = 1;
      
                  string filename = String.Format(recordFilenameFormat, counter.ToString());
      
                  while (this.FileManager.FileExists(filename))
                  {
                      this.FileManager.DeleteFile(filename);
      
                      counter++;
                      filename = String.Format(recordFilenameFormat, counter.ToString());
                  }
      
                  if (reply == ReturnCode.Success)
                  {
                      channel.FilePlayer.Say("All messages deleted.");
                  }
      
                  return reply;
              }
      
              // Returns: Success if a valid key was selected, CallHungUp if call hung up.
              private ReturnCode GetMenuSelection(UASCallChannel channel, string prompt, int secondsRepeatPrompt, string validKeys, out char keySelected)
              {
                  keySelected = '\0';
      
                  // Prepare to receive digits
                  channel.DtmfDetector.ClearDigits();
      
                  do
                  {
                      FilePlayerCause cause = channel.FilePlayer.Say(prompt, true);
                      if (cause == FilePlayerCause.HangUp)
                      {
                          return ReturnCode.CallHungUp;
                      }
      
                      string digits;
                      channel.DtmfDetector.GetDigits(1, out digits, secondsRepeatPrompt);
                      if (channel.DtmfDetector.Cause == DtmfDetectorCause.Count)
                      {
                          if (validKeys.Contains(digits))
                          {
                              this.Trace.TraceInfo("Got keypress {0}", digits[0]);
                              keySelected = digits[0];
                              return ReturnCode.Success;
                          }
                          else
                          {
                              channel.FilePlayer.Say("Selection not valid.");
                          }
                      }
                  } while (channel.DtmfDetector.Cause != DtmfDetectorCause.HangUp);
      
                  // returning on hangup
                  return ReturnCode.CallHungUp;
              }
      
              private string voicemailBoxName;
              private string recordFilenameFormat;
              private int recordedFileCounter;
          }
      }
                                  
    • Sample Files:
      • Samples\VB\Voicemail3\Voicemail3.vb

      Specifying a Voicemail Box

      This step makes use of the applicationArguments argument that contains the user-defined string originating from the Inbound Service configuration. This string is expected to contain the name we have set up to be our voicemail box. We set up a format for all voicemail filenames that locates them in a voicemailbox subfolder on the cloud.

      After answering the call the application identifies the level of access for the caller. It does this by comparing the caller Id with the voicemailBox name, as determined from applicationArguments. For SIP calls, the voicemailBox name is matched with the Username of the caller (the string before the '@' character) while for PSTN calls the caller Id is simply the calling number.

      If the caller Id and voicemailBox name match then the caller is deemed to be the owner of the voicemail box and the application calls TopLevelMenu() that allows the caller to listen and delete all messages. Otherwise the caller is simply allowed to record a new message.

      File checking

      In the Run() method we check for the existence of a file using the FileManager.FileExists() method. We continually increment the recordedFileCounter variable until we identify a free filename - this is our next filename for recording.

      Dealing with DTMF

      For menu entry via the keypad, a new method has been created called GetMenuSelection(). This reads out a prompt, passed as a parameter, a predigit timeout value, and also any valid selections that the user is able to press. A string of the single digit reply is returned. In the TopLevelMenu() method, pressing a '1' will call the PlayAllMessages() method and pressing a '2' will call DeleteAllMessage().

      Voicemail functionality

      In PlayAllMessages(), a counter is incremented from 1, testing with a derived filename is the file exists, and if so, will play the file. This continues until there are no more messages. DeleteAllMessage() does a similar job, testing for the existence of a derived filename, but rather than play it, will delete the file with the FileManager.DeleteFile() method.

      The RecordNewMessage() method behaves like the record method in the previous step, except that it now uses the GetMenuSelection method to determine user input.

    • Imports AMSClassLibrary
      Imports UASAppAPI
      
      ' An inbound application that answers the call, says "Welcome to the
      ' voicemail server for <name>."
      ' <name> is obtained from the applicationArguments.
      ' It is compared with the <callFrom> property of the inbound call.
      '
      ' If <callFrom> equals <name>
      '     Say "Press 1 to hear all messages, press 2 to delete all messages".
      '     1. Play all existing messages
      '     2. Delete all existing messages
      ' else
      '     Do recording as in voicemail 2.
      '
      ' All options return to the above menu.
      ' The app only completes when the call is hung up.
      ' The name of the .wav file to save to will be "samples/voicemail3/name/msg<counter>.wav".
      ' The <counter> is a variable, initialised to 1, which increments each time a
      ' message is successfully saved.
      '
      ' Requires:
      ' [applicationParameters = voicemail box name]
      Namespace Voicemail3
      
          ' The application class.
          ' This must have the same name as the assembly and must inherit from either
          ' UASInboundApplication or UASOutboundApplication.
          ' It must override the Run method.
          Public Class Voicemail3
              Inherits UASInboundApplication
      
              ' Possible return codes
              Enum ReturnCode
                  ' Success Codes:
                  Success = 0
                  RecordedAMessage = 1
                  CallHungUp = 2
      
                  ' Fail Codes:
                  ' -1 to -99 reserved
                  ExceptionThrown = -100
                  ArgumentEmpty = -101
                  RecordingFailedToStart = -102
              End Enum
      
              ' This is the entry point for the application
              Overrides Function Run(ByVal channel As UASCallChannel, _
                                     ByVal applicationParameters As String) _
                                     As Integer
      
                  Me.Trace.TraceInfo("Started")
                  Dim reply As ReturnCode = ReturnCode.Success
      
                  ' The voicemail box name must have been configured at service registration
                  voicemailBoxName = applicationParameters
                  If voicemailBoxName.Length = 0 Then
                      Return ReturnCode.ArgumentEmpty
                  End If
      
                  recordFilenameFormat = "samples/voicemail3/" + voicemailBoxName + "/msg{0}.wav"
      
                  ' Obtain the next free counter
                  recordedFileCounter = 1
                  While Me.FileManager.FileExists(String.Format(recordFilenameFormat, recordedFileCounter.ToString()))
                      recordedFileCounter += 1
                  End While
      
                  Try
                      ' Answer the call
                      Dim state As CallState
                      state = channel.Answer()
                      If state = CallState.Answered Then
                          Dim caller = GetCaller(channel)
                          Me.Trace.TraceInfo("Call from {0} to {1}'s Voicemail answered", _
                                                      caller, voicemailBoxName)
      
                          PlayWelcomeMessage(channel, voicemailBoxName)
      
                          If caller = voicemailBoxName Then
                              TopLevelMenu(channel)
                          Else
                              RecordNewMessage(channel)
                              channel.FilePlayer.Say("Goodbye.")
                          End If
                      End If
      
                  Catch ex As Exception
                      Me.Trace.TraceError("Exception thrown {0}", ex.Message)
                      reply = ReturnCode.ExceptionThrown
                  Finally
                      channel.HangUp()
                  End Try
      
                  Me.Trace.TraceInfo("Completed")
                  Return reply
      
              End Function
      
              Private Function PlayWelcomeMessage(ByVal channel As UASCallChannel, ByVal name As String) As ReturnCode
                  channel.FilePlayer.Say("Welcome to the voicemail server for {0}", name)
                  Return ReturnCode.Success
              End Function
      
              Private Function GetCaller(ByVal channel As UASCallChannel) As String
                  ' Extract the user part of a SIP address
                  Dim caller = channel.CallDetails.CallFrom
                  If caller.Contains("sip:") Then
                      caller = caller.Substring(4)
                      Dim pos = caller.IndexOf("@")
                      If (pos >= 0) Then
                          caller = caller.Remove(pos)
                      End If
                  End If
                  Return caller
              End Function
      
              Private Function TopLevelMenu(ByVal channel As UASCallChannel) As ReturnCode
                  Dim reply = ReturnCode.Success
                  Dim prompt = "Press 1 to listen to all messages. " + _
                               "Press 2 to delete all messages. " + _
                               "Press hash to exit."
                  Dim keySelected As Char
      
                  Do
                      reply = GetMenuSelection(channel, prompt, 20, "12#", keySelected)
                      If reply = ReturnCode.Success Then
                          Select Case keySelected
                              Case "1"
                                  reply = PlayAllMessages(channel)
                              Case "2"
                                  reply = DeleteAllMessages(channel)
                              Case "#"
                                  Return reply
                          End Select
                      End If
                  Loop While reply = ReturnCode.Success
      
                  Return reply
              End Function
      
              Private Function RecordNewMessage(ByVal channel As UASCallChannel) As ReturnCode
                  Dim reply = ReturnCode.Success
      
                  Dim filename = String.Format(recordFilenameFormat, recordedFileCounter.ToString())
                  recordedFileCounter += 1
      
                  ' Loop round until have recorded a message acceptable to the caller
                  ' or caller has hung up
                  Dim satisfiedWithMessage = False
                  Do
                      ' Prompt to leave a message
                      channel.FilePlayer.Say("Please record a message after the tone. " + _
                                             "Press any digit to stop the recording.")
      
                      ' Delay before starting the recording and playing the tone (for effect)
                      Thread.Sleep(1000)
      
                      ' Clear any received digits
                      channel.DtmfDetector.ClearDigits()
      
                      ' Start recording, enabling barge in
                      Me.Trace.TraceInfo("Recording message to file: [{0}]", filename)
                      If Not channel.FileRecorder.Start(filename, True) Then
                          Me.Trace.TraceError("Recording failed to start cause = {0}", _
                                                channel.FileRecorder.Cause)
                          reply = ReturnCode.RecordingFailedToStart
                          Exit Do
                      End If
      
                      ' Play a tone to signal that recording has started
                      channel.DtmfPlayer.Play("0")
      
                      ' Wait for a period for recording to complete or
                      ' the call to have been hung up
                      Dim recCause = channel.FileRecorder.WaitUntilFinished(60)
                      If recCause = FileRecorderCause.HangUp Then
                          Me.Trace.TraceInfo("Call has been hung up")
                          reply = ReturnCode.CallHungUp
                          Exit Do
                      End If
                      If recCause = FileRecorderCause.Timeout Then
                          Me.Trace.TraceInfo("Recording has stopped after 60 seconds")
                          channel.FileRecorder.Stop()
                      End If
      
                      ' Replay the recorded message
                      channel.FilePlayer.Say("The following message has been recorded")
                      If channel.FilePlayer.Play(filename, True) = FilePlayerCause.HangUp Then
                          Me.Trace.TraceInfo("Call has been hung up")
                          reply = ReturnCode.CallHungUp
                          Exit Do
                      End If
      
                      ' Check if the user is satisfied with the recorded message
                      Dim prompt = "Please press 1 to keep this message or 2 to record an " + _
                                      "alternative message"
                      Dim keySelected As Char
      
                      reply = GetMenuSelection(channel, prompt, 20, "12", keySelected)
                      If reply = ReturnCode.Success Then
                          If keySelected = "1" Then
                              satisfiedWithMessage = True
                              channel.FilePlayer.Say("Message saved.")
                          End If
                      End If
                  Loop While Not satisfiedWithMessage And (reply = ReturnCode.Success)
      
                  Return reply
              End Function
      
              Private Function PlayAllMessages(ByVal channel As UASCallChannel) As ReturnCode
                  Dim reply = ReturnCode.Success
                  Dim counter = 1
      
                  Dim filename = String.Format(recordFilenameFormat, counter.ToString())
      
                  While Me.FileManager.FileExists(filename)
                      channel.FilePlayer.Say("Message " + counter.ToString())
      
                      channel.FilePlayer.Play(filename)
                      If channel.FilePlayer.Cause = FilePlayerCause.HangUp Then
                          reply = ReturnCode.CallHungUp
                          Exit While
                      End If
      
                      counter += 1
                      filename = String.Format(recordFilenameFormat, counter.ToString())
                  End While
      
                  If reply = ReturnCode.Success Then
                      channel.FilePlayer.Say("No more messages.")
                  End If
      
                  Return reply
              End Function
      
              Private Function DeleteAllMessages(ByVal channel As UASCallChannel) As ReturnCode
                  Dim reply = ReturnCode.Success
                  Dim counter = 1
      
                  Dim filename = String.Format(recordFilenameFormat, counter.ToString())
      
                  While Me.FileManager.FileExists(filename)
                      Me.FileManager.DeleteFile(filename)
      
                      counter += 1
                      filename = String.Format(recordFilenameFormat, counter.ToString())
                  End While
      
                  If reply = ReturnCode.Success Then
                      channel.FilePlayer.Say("All messages deleted.")
                  End If
      
                  Return reply
              End Function
      
              ' Returns: Success if a valid key was selected, CallHungUp if call hung up.
              Private Function GetMenuSelection(ByVal channel As UASCallChannel, ByVal prompt As String, ByVal secondsRepeatPrompt As Integer, ByVal validKeys As String, ByRef keySelected As Char) As ReturnCode
                  keySelected = "\0"
      
                  ' Prepare to receive digits
                  channel.DtmfDetector.ClearDigits()
      
                  Do
                      Dim cause = channel.FilePlayer.Say(prompt, True)
                      If cause = FilePlayerCause.HangUp Then
                          Return ReturnCode.CallHungUp
                      End If
      
                      Dim digits As String = ""
                      channel.DtmfDetector.GetDigits(1, digits, secondsRepeatPrompt)
                      If channel.DtmfDetector.Cause = DtmfDetectorCause.Count Then
                          If (validKeys.Contains(digits)) Then
                              Me.Trace.TraceInfo("Got keypress {0}", digits(0))
                              keySelected = digits(0)
                              Return ReturnCode.Success
                          Else
                              channel.FilePlayer.Say("Invalid selection.")
                          End If
                      End If
                  Loop While channel.DtmfDetector.Cause <> DtmfDetectorCause.HangUp
      
                  ' returning on hangup
                  Return ReturnCode.CallHungUp
              End Function
      
              Private voicemailBoxName As String
              Private recordFilenameFormat As String
              Private recordedFileCounter As String
      
          End Class
      
      End Namespace
                                  

Next time...

So, we have a simple bare-bone voicemail system up and running. We could personalise the voicemail box some more though. How about allowing the owner of the voicemail box to record their own introduction message and use the file as a welcome message? Additionally we could allow the owner to select messages to keep or delete. Read on to find out how...

Lesson 4