The applications tutorial

note:

In this tutorial we assume that the UAS and the UAS management console are up and running and that the UAS has successfully connected to cloud.aculab.com.

The User Application Service (UAS) is the process that runs applications written by the user. The UAS Management Console (MC) is a web services interface to the UAS. The MC is driven through a web browser and is used to install applications on the UAS.

The primary goal of an application is to handle a single telephone call.

Introduction

After successfully connecting to cloud.aculab.com, the UAS will start to receive other commands. One of these commands might be to start up an application. If the UAS has installed the application in question, it will attempt to run it. The application will be one that was written by the user in Python using the API provided. The API is described below.

Please note that for an application to be targeted by cloud.aculab.com, it must first be registered against a service on cloud.aculab.com. Furthermore, there are a few rules that the application writer needs to be aware of, we will cover these in this tutorial.

The application version

Each application should have a version number. The application writer should define a global variable __uas_version__ which is a string that contains the version number. For example:

__uas_version__ = "0.0.1b"

The application identifier

Each application must have an identifier. The application writer must define a global variable __uas_identify__ which is a string that contains the word application. Source files that contain common code intended to be imported by applications, are also installed on the UAS. These files must also have an identifier and the identifier must be the string common. For example:

__uas_identify__ = "application"

or:

__uas_identify__ = "common"

The main function

Each application must have a main function. The UAS will attempt to execute main with a number of arguments. The arguments to the application are provided by cloud.aculab.com. See the section on applications for more information on main’s arguments.

A very basic inbound application

An inbound application will be launched when an inbound call is detected. We can ring and answer the call like this:

__uas_identify__ = "application"
__uas_version__  = "0.0.1b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    channel.ring()   # ring for a couple of seconds
    channel.answer() # answer the call

    # We have answered the call, wait for it to he hung up
    channel.wait_for_idle()

return 0

Checking the call state

A call channel will have a particular state. When a call channel is not connected its state will be IDLE, when it is connected its state will be ANSWERED.

Sometimes it is important to check the call channel state, this is done as follows:

__uas_identify__ = "application"
__uas_version__  = "0.0.2b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    # we can check the current state like this:
    state = channel.state() # retrieve the current call state
    if state == channel.State.CALL_INCOMING:
        channel.ring()
        channel.answer()
        channel.wait_for_idle()
        return 0
    # the call was not in the correct state
    return -101

The Hangup exception

In the previous example we check that the call state is CALL_INCOMING before answering. We do this because the call might already have gone to IDLE, in which case we’d probably want to raise a Hangup exception. In fact, both ring() and answer() will raise a Hangup exception if the call goes to IDLE, so we have to be prepared to catch it. In order to use the Hangup exception (and later the Error exception) we need to import it from the prosody.uas module as shown below.

In this example we explicitly raise a Hangup exception and catch it:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.2b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    try:
        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            # raise Hangup exception, call is probably IDLE
            raise Hangup("No call")

        # do some interesting call stuff here and then ...
        channel.wait_for_idle()

    except Hangup as exc:
        # do some post Hangup exception processing, e.g. logging
        pass
    finally:
        # this finally will always be run, we can use this to
        # ensure that a call channel is hung up on application exit
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return 0

In this example we have put the call control code in a try except finally block. We have also introduced the use of state() to check whether a call channel needs to be hung up on application exit.

The hang_up function waits for the IDLE state, but this function might time out, so a state of IDLE cannot be guaranteed after calling the hang_up function.

We have also introduced explicitly raising a Hangup exception. Raising a Hangup exception does not cause the call to be hung up, but it is a convenient way to terminate an application after detecting an unwanted call state. Using the finally clause is a convenient way to ensure that an application will always hang up and return a value.

The Error exception

The Error exception is raised when the application has seen something undesirable and needs to exit.

If, for instance, a particular function does not exit as expected, the application might not want to continue:

# Assuming a call has been established
# Start playing a file, if the file won't play raise an Error exception
if channel.FilePlayer.start(filename="playfile.wav") is False:
    raise Error("playback failed to start")

It is important to catch the Error exception in the same way that Hangup exceptions are caught. This will allow the application to log the exception and also return a suitable error code. Expanding on the previous example:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.2b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    try:
        return_code = 0

        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            # raise Hangup exception, call is probably IDLE
            raise Hangup("No call")

        if channel.FilePlayer.start(filename="playfile.wav") is False:
            raise Error("playback failed to start")

        channel.wait_for_idle()

    except Hangup as exc:
        my_log.info("Caught Hangup exception: {0}".format(exc))
        return_code = -102

    except Error as exc:
        # log the error and set a suitable return code
        my_log.info("Caught Hangup exception: {0}".format(exc))
        return_code = -101

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()

    return return_code

Creating loops within an application

A recurring problem among application writers is inadvertently creating infinite loops. The API provides a module that can be used to check whether the application needs to quit. In many cases an application loop will be calling other API functions, and these will raise a Hangup exception if the call associated with the channel goes idle, thus exiting the loop. However, if no API functions are being called, this module can be used, as shown in the following example of a common wait function:

__uas_identify__ = "common"
__uas_version__ = "1.0b1"

import time
# import the module
from prosody.uas import ICanRun

def my_common_wait(sleep_seconds):
    # create an instance of the class
    i_can_run = ICanRun(channel)
    end_time = time.time() + sleep_seconds
    # sleep for required time or until the check fails.
    while time.time() < end_time and i_can_run:
        time.sleep(1)

Introducing the logger

A reference to the UAS’s logger is passed into the main function. The logger can be used to write trace to the UAS’s log file at several log levels. Let’s write some log lines:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.3b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    try:
        if channel.state() == channel.State.ANSWERED:
            # do some interesting call stuff here ...
            my_log.info("Answered an inbound call") # log at info level
            channel.wait_for_idle()
        else:
            my_log.warning("didn't get a call")
            raise Hangup('No call')

    except Hangup as exc:
        my_log.info("Caught Hangup exception: {0}".format(exc))

    finally:
        if channel.state() != channel.State.IDLE:
            my_log.info("Hang up the call")
            channel.hang_up()
    return 0

The outbound call

Inbound and outbound applications share much of the same functionality, but outbound applications have a slightly different main function. And, of course, outbound applications need to place a call:

state = channel.call("sip:1234@127.0.0.1:5060", call_from="bob@1234") # default timeout of 60 seconds

The function will place a SIP call and return once the destination has responded or a timeout has occurred. If the call is answered the call state will be ANSWERED. If the function times out the call cause will be TIMEOUT. Other states, such as BUSY are also possible.

Later, if required, we can hang up the call in the normal way:

channel.hang_up()

An example of placing a PSTN call is:

state = channel.call("tel:441908273800", call_from="441234567890")

There are a number of rules that go with making outbound PSTN calls. Please read the information on cloud.aculab.com.

Here is an example of making an outbound call in a loop. The call function can return for several reasons, one of those can be the cause BUSY. In this example, if the cause is BUSY, the call is attempted again a number of times:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.4b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        # we will try for one minute
        endTime = time.time() + 60
        # In this app, the call destination is supplied
        # in the outbound_parameters string, and the call
        # origin is supplied in application_parameters.
        destination = outbound_parameters
        origin = application_parameters

        while channel.call(destination, call_from=origin) != channel.State.ANSWERED:
            cause = channel.cause()
            if cause != channel.Cause.BUSY:
                raise Hangup("Call destination returned cause {0}".format(cause))
            if time.time() > endTime:
                raise Hangup("Call destination is busy.")
            time.sleep(10)
            continue

        my_log.info("outbound call connected")
        # do some interesting call stuff here and then ...
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.info("Caught Hangup exception: {0}".format(exc))

    finally:
        if channel.state() != channel.State.IDLE:
            my_log.info("Hang up the call")
            channel.hang_up()
    return 0

The option to start an outgoing call in a non-blocking manner is the start_call function. This will return True once confirmation has been received that the outgoing call is in progress. We then have the option to do something else, perhaps while polling the outgoing call’s state. To rejoin the call and wait until it has been answered (or gone to IDLE) we can call wait_for_outgoing_call:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.5b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        destination = outbound_parameters
        origin = application_parameters
        channel.start_call(destination, call_from=origin)

        # This will not block, so now we can do something else.
        # ...
        # At some point we need to rejoin the outgoing call and wait
        # for the ANSWERED or IDLE state.

        state = channel.wait_for_outgoing_call()
        if state != channel.State.ANSWERED:
            raise Hangup("Call was not answered")

        my_log.info("outbound call connected")
        # do some interesting call stuff here and then ...
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.info("Caught Hangup exception: {0}".format(exc))

    finally:
        if channel.state() != channel.State.IDLE:
            my_log.info("Hang up the call")
            channel.hang_up()
    return 0

SIP Headers, INFO and MESSAGE

The channel.SIP attribute can be used to send and receive SIP Headers and also INFO and MESSAGE packages.

At the time of writing, only SIP Headers are supported on Aculab Cloud. Please refer to the online documentation for updates.

For sending SIP Headers, HeaderFieldsToSend list is exposed on channel.SIP. Append SIPHeaderField objects to this list before the call is placed or answered:

from prosody.uas import SIPHeaderField

# create a header field object, currently only custom headers, i.e., those beginning with x- can be sent.
header_field = SIPHeaderField('x-aculab_header', 'a test header value')
# a header field object can hold more than one value
header_field.add_value('another test header value')

# append the header field to the list of header fields to send
channel.SIP.HeaderFieldsToSend.append(header_field)

Once a call has been answered, received header fields can be requested by name, these are returned as SIPHeaderField objects:

# the request will return a SIPHeaderField object that can hold more than one value
header_field = channel.SIP.request_header_field("x-aculab_header")

# the values are returned in a list
header_field_values = header_field.get_values()

for header_field_value in header_field_values:
    print("Value: {0}".format(header_field_value)

Note: INFO and MESSAGE are not yet supported, Please refer to the online documentation for updates.

For sending SIP INFO and MESSAGE packages, channel.SIP has the function send. This function will take either a SIPInfo object or a SIPMessage object:

from prosody.uas import SIPInfo, SIPMessage

# to send a SIP INFO package
channel.SIP.send(SIPInfo("Alexey Fyodorovitch Karamazov was the third son of Fyodor Pavlovitch Karamazov.", "text/plain; charset=ascii"))

# to send a SIP MESSAGE package
channel.SIP.send(SIPMessage("Call me Ishmael.", "text/plain; charset=ascii"))

A special helper class is available for sending SIP INFO DTMF packages:

from prosody.uas import SIPInfoDTMFRelay

# send a DTMF digit, specify the digit and milliseconds duration
channel.SIP.send(SIPInfoDTMFRelay("#", 250))

To get SIP INFO and MESSAGE packages channel.SIP has the functions get_infos and get_messages. These functions return a list of SIPInfo and SIPMessage objects respectively:

from prosody.uas import SIPInfo, SIPMessage

# get a list of SIPInfo objects
sip_infos = channel.SIP.get_infos()

for sip_info in sip_infos:
    info_body = sip_info.get_body()
    print("Body: {0}".format(info_body))

# get a list of SIPMessage objects
sip_messages = channel.SIP.get_messages()

for sip_message in sip_messages:
    message_body = sip_message.get_body()
    print("Body: {0}".format(message_body))

Media recording and playback

Once a call is connected, it it possible to record the audio to a file.

Some of the examples given below will play a file. The file must reside on cloud.aculab.com and can be manually uploaded (or downloaded) via the file management page of cloud.aculab.com - while logged in to cloud.aculab.com, click Manage then Media Files.

Please also have a look at the Web Services API for instructions and examples on automatically uploading files to cloud.aculab.com.

Please note, before working with media files it is important that you also read the section about media files which describes some of the effects of Eventual Consistency.

Using the outbound call example:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.6b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        endTime = time.time() + 60 # we will try for one minute
        destination = outbound_parameters
        origin = application_parameters
        while channel.call(destination, call_from=origin) != channel.State.ANSWERED:
            cause = channel.cause()
            if cause != channel.Cause.BUSY:
                raise Hangup("Call destination returned cause {0}".format(cause))
            if time.time() > endTime:
                raise Hangup("Call destination is busy.")
            time.sleep(10)
            continue

        # the call has been answered, record some audio
        channel.FileRecorder.record(filename="recfile_{0}.wav".format(application_instance_id))
        # now wait for the call to hang up
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.warning("Caught a Hangup exception: {0}".format(exc))

    finally:
        if channel.state() != channel.State.IDLE:
            my_log.info("Hang up the call")
            channel.hang_up()
    return 0

Recording audio will create a WAV file which is stored in a pre-configured directory. The example above uses the default settings which will allow, approximately, a two minute recording.

To see why the recording terminated we check the cause, for example:

cause = channel.FileRecorder.record(filename="recfile.wav")
if cause == channel.FileRecorder.Cause.SILENCE:
    # recording stopped due to silence on the line
    pass

It is possible to check whether any audio was recorded to the file:

if channel.FileRecorder.audio_detected_in_recording() is True:
    # Yes, some audio was recorded
    pass
else:
    # No, the recording contains only silence
    pass

Please note that the check for audio can only be done after the recording job has ended.

Similarly, a WAV file can be played to the call:

cause = channel.FilePlayer.play(filename="playfile.wav")
if cause != channel.FilePlayer.Cause.NORMAL:
    # play did not stop normally
    pass

This will play a file that has already been uploaded to cloud.aculab.com.

The two functions described above will block until the recording or playback is complete, this is not always desirable and so non-blocking functions are also available. To start a playback, and return as soon as it has started, we do the following:

# Assuming a call has been established
if channel.FilePlayer.start(filename="playfile.wav") is False:
    # On errors we can raise a Error exception
    raise Error("playback failed")
else:
    # OK, playback has started we can now do something else
    pass

To track the playback progress we can check the state:

while channel.FilePlayer.state() == channel.FilePlayer.State.PLAYING:
    # continue to do something here
    pass

If no playback is in progress when the state is checked, the state function will return IDLE.

Or we can wait until playing is complete:

if channel.FilePlayer.wait_until_finished() == channel.FilePlayer.Cause.NORMAL:
    # player stopped normally
    pass

Encryption and Decryption

To encrypt or decrypt files that are going to be played or recorded use the cipher class. Create the cipher first, providing a key and an initialisation vector.

This example records audio to an encrypted file, using an inbound call example:

from prosody.uas import Hangup, Error, AESCBCCipher

__uas_identify__ = "application"
__uas_version__  = "0.0.6b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        return_code = 0

        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            # raise Hangup exception, call is probably IDLE
            raise Hangup("No call")

        # The call has been answered, record some audio and encrypt it
        my_cipher = AESCBCCipher(key="3e139a97574248d43bf01de3521474bf69f32ec2fc00edb866c26dcffff9d0a3",
                                 vector="9b0ef243445da908976b7dc9c247c29e")

        channel.FileRecorder.record(filename="my_encrypted.wav", cipher=my_cipher)

    except Hangup as exc:
        my_log.warning("Caught a Hangup exception: {0}".format(exc))

    except Error as exc:
        my_log.error("Caught an Error exception: {0}".format(exc))
        return_code = -101

    finally:
        if channel.state() != channel.State.IDLE:
            my_log.info("Hang up the call")
            channel.hang_up()

    return return_code

This example plays audio from an encrypted file, using an inbound call example:

from prosody.uas import Hangup, Error, AESCBCCipher

__uas_identify__ = "application"
__uas_version__  = "0.0.6b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        return_code = 0

        # This application will play an encrypted file.
        # The file must be validated before it can be played.
        # Validation should happen before the call is answered.
        # The key and vector below are examples, do not use these values.
        my_cipher = AESCBCCipher(key='3e139a97574248d43bf01de3521474bf69f32ec2fc00edb866c26dcffff9d0a3',
                                 vector='9b0ef243445da908976b7dc9c247c29e')

        val = file_man.validate(filename=my_file_name, cipher=my_cipher)
        if val != file_man.ValidateResultType.VALIDATED:
            raise Error("Could not validate encrypted file: {0}".format(val))

        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            # raise Hangup exception, call is probably IDLE
            raise Hangup("No call")

        channel.FilePlayer.play(filename=my_file_name, cipher=my_cipher)

    except Hangup as exc:
        my_log.warning("Caught a Hangup exception: {0}".format(exc))

    except Error as exc:
        my_log.error("Caught an Error exception: {0}".format(exc))
        return_code = -101

    finally:
        if channel.state() != channel.State.IDLE:
            my_log.info("Hang up the call")
            channel.hang_up()

    return return_code

Text to Speech

Instead of playing a file, an application can speak some text. Getting the application to speak text is very similar to playing a file. The difference is that the play() function is replaced with a say() function. And, instead of providing a file name, a string of text to say is provided.

Here is an example of a speaking clock:

from prosody.uas import Hangup, Error
import time

__uas_identify__ = "application"
__uas_version__  = "0.0.6b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        return_code = 0

        endTime = time.time() + 60 # we will try for one minute
        destination = outbound_parameters
        origin = application_parameters
        while channel.call(destination, call_from=origin) != channel.State.ANSWERED:
            cause = channel.cause()
            if cause != channel.Cause.BUSY:
                raise Hangup("Call destination returned cause {0}".format(cause))
            if time.time() > endTime:
                raise Hangup("Call destination is busy.")
            time.sleep(10)
            continue

        # the call has been answered, play some Text To Speech
        channel.FilePlayer.say("Welcome to the speaking clock")
        if cause != channel.FilePlayer.Cause.NORMAL:
            raise Error("TTS player returned {0}: expected {1}".format(cause, channel.FilePlayer.Cause.NORMAL))

        date = time.strftime("%Y/%m/%d", time.localtime(time.time()))

        # TTS also supports SSML tags which can change the way the text is spoken. See /documents/tts#
        # here we use SSML to specify some text as a date.
        cause = channel.FilePlayer.say("The date is <say-as interpret-as='date' format='ymd'>{0}</say-as>".format(date))
        if cause != channel.FilePlayer.Cause.NORMAL:
            raise Error("TTS player returned {0}: expected {1}".format(cause, channel.FilePlayer.Cause.NORMAL))

        timestr = time.strftime("%H:%M:%S", time.localtime(time.time()))

        # here we use SSML to specify a particular TTS engine and voice, and also to specify some text as the time.
        cause = channel.FilePlayer.say("<acu-engine name='Ivona'><voice name='Brian'>The time is <say-as interpret-as='time' format='hms24'>{0}</say-as></voice></acu-engine>".format(timestr))
        if cause != channel.FilePlayer.Cause.NORMAL:
            raise Error("TTS player returned {0}: expected {1}".format(cause, channel.FilePlayer.Cause.NORMAL))

        # now wait for the call to hang up
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.warning("Caught a Hangup exception: {0}".format(exc))

    except Error as exc:
        my_log.error("Caught an Error exception: {0}".format(exc))
        return_code = -101

    finally:
        if channel.state() != channel.State.IDLE:
            my_log.info("Hang up the call")
            channel.hang_up()

    return return_code

Whole call recording

There are times when you’d like to record an entire call - both sides of the conversation. And ordinary record job will not do this for you. For this task we have the whole call recorder.

The following example shows how to start the whole call recorder, then stop it and wait for it to complete:

# start record, then call stop and wait
if channel.WholeCallRecorder.start(filename="whole_call.wav") != True:
    raise Error('whole call record start failed')
# here we can do some other application jobs, including
# play and record, and DTMF
if channel.WholeCallRecorder.stop() != True:
    raise Error('whole call record stop failed')
cause = channel.WholeCallRecorder.wait_until_finished()
if cause != channel.WholeCallRecorder.Cause.ABORTED:
    raise Error("file record returned {0}: expected {1}".format(cause, channel.WholeCallRecorder.Cause.ABORTED))

Dual Tone Multi-Frequency (DTMF)

In much the same way that we can play and record audio files, we can play and detect DTMF digits. DTMF detection is used in IVR menus ( press 1 for this, press two for that, etc. ); note that the high level API has a function that simplifies writing an application that implements an IVR menu.

The following example shows a channel detecting five DTMF digits and then playing them back:

# wait at most 10 seconds for the first digit to be detected
dtmf = channel.DTMFDetector.get_digits(count=5, seconds_predigits_timeout=10)
if channel.DTMFDetector.cause() == channel.DTMFDetector.Cause.COUNT:
    channel.DTMFPlayer.play(dtmf)

In this example the cause should be COUNT if five digits are counted. If the timeout occurs first, the cause will be TIMEOUT.

The following example shows a channel playing four DTMF digits; the player will return a cause:

if channel.DTMFPlayer.play('1234') != channel.DTMFPlayer.Cause.NORMAL:
    # DTMF play did not terminate normally
    pass

Speech Detection (ASR)

Sometimes it is easier for the caller to say a digit than press a key. With the speech detector you can enable Automatic Speech Recognition for tasks such as “please say your age”. The application will supply a grammar, please see the website for details, and the speech detector will try to match the caller’s speech with the grammar.

The following example sets up the speech detector to recognise a number from sixteen to ninety nine (an age range) using a pre-defined grammar. It also copes with the caller pressing digits on the telephone keypad instead of talking:

# Create the grammar object.
my_grammar = channel.SpeechDetector.Grammar()

# Create a grammar from one that is pre-defined.
my_grammar.create_from_predefined('SixteenToNinetyNine')

# Prime the speech detector to start listening once the next play has finished,
# this allows a prompt (or question) to be played which is immediately followed by
# the speech detector listening to the response.
if channel.SpeechDetector.prime(my_grammar, channel.SpeechDetector.SpeechDetectorTrigger.ONPLAYEND) is False:
    raise Error("Speech detector failed to start")

# Say the TTS prompt, the text will ask the caller their age; assumed to be a value from sixteen to ninety nine.
cause = channel.FilePlayer.say("How old are you?")
if cause != channel.FilePlayer.Cause.NORMAL:
    raise Error("Say prompt failed: cause is {0}".format(cause))

# Now get the recognised speech. This returns a UASSpeechDetectorResult object
caller_age = None
response = channel.SpeechDetector.get_recognised_speech()
if not response:
    # no speech response, the speech detector may have been interrupted by DTMF
    cause = channel.SpeechDetector.cause()
    if cause == channel.SpeechDetector.Cause.BARGEIN:
        # there should be two DTMF digits to collect
        caller_age = channel.DTMFDetector.get_digits(count=2)
else:
    caller_age = response.get_recognised_words_as_string()

File handling

The API offers some basic file and directory handling functions for the file system on cloud.aculab.com based. These can be useful in conjunction with the media record and playback functions.

In this example we decide to delete a recording if the cause is not what we expected, we also then raise an Error exception and return a negative return code:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.7b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        return_code = 0
        endTime = time.time() + 60 # we will try for one minute
        destination = outbound_parameters
        origin = application_parameters
        while channel.call(destination, call_from=origin) != channel.State.ANSWERED:
            cause = channel.cause()

            # Here we raise an Error exception because of an unexpected call cause
            if cause != channel.Cause.BUSY:
                raise Error("Call destination returned cause {0}".format(cause))

            if time.time() > endTime:
                raise Hangup("Call destination is busy.")
            time.sleep(10)
            continue

        # the call has been answered, record some audio
        cause = channel.FileRecorder.record(filename="recfile_{0}.wav".format(application_instance_id))
        if cause != channel.FileRecorder.Cause.SILENCE:
            # we don't like this so we delete the recording
            file_man.delete_file("recfile_{0}.wav".format(application_instance_id))
            raise Error("recording terminated due to {0}".format(cause))

        # now wait for the call to hang up
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.warning("Caught a Hangup exception: {0}".format(exc))
        return_code = 100

    except Error as exc:
        my_log.error("Caught an error exception: {0}".format(exc))
        return_code = -100

    except Exception as exc:
        # this will also log some stack trace
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            my_log.info("Hang up the call")
            channel.hang_up()
    return return_code

The extra channel

The primary call channel, which is passed into the main function, provides a reference to a secondary call channel referred to as the extra channel. The extra channel can make outbound calls, but it cannot receive inbound calls.

The extra channel is provided so that an application can make an outbound call if required. The example below demonstrates using the extra channel.

Connecting calls

Two calls can be connected together to allow the two parties to talk to each other. Also note that the high level API has a function that simplifies writing an application that connects two calls together.

The following example demonstrates an inbound application placing an outbound call (using the extra channel) and connecting the two calls together. Here we will also be catching the Error exception, and returning a negative value to indicate to the UAS that the application failed:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.8b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    return_code = 0
    out_channel = None
    try:
        try:
            out_channel = channel.ExtraChannel[0]
        except:
            raise Error('could not get extra channel')

        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            raise Hangup('No inbound call')

        my_log.info("Answered an inbound call")
        # place an outbound call using the extra channel, the destination
        # and origin of the call are given in application_parameters
        # separated by a space.
        destination, origin = application_parameters.split()
        if out_channel.call(destination, call_from=origin) != channel.State.ANSWERED:
            raise Hangup('No Outbound')

        # We have an outbound call, let the two parties talk to each other
        if channel.connect(out_channel) is True:
            # The calls are connected, the two endpoints can now talk to each other.
            # After some time we may want to disconnect the calls.
            channel.disconnect()
            # it would also be legal to call instead ...
            # out_channel.disconnect()
        else:
            raise Error('Connect failed')

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = 100

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
        if out_channel is not None:
            if out_channel.state() != out_channel.State.IDLE:
                out_channel.hang_up()
    return return_code

To connect two calls in this way their states must be ANSWERED. The disconnect function can be called on either call channel. While the call channels are in the connected state, they cannot be used to play media (files or DTMF); but they can be used to record media or to detect DTMF. Similarly, while a call channel is playing, it cannot be used to connect to another call channel.

Below is a more complex example, combining call handling with some media functions:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.9b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    return_code = 0
    out_channel = None
    try:
        try:
            out_channel = channel.ExtraChannel[0]
        except:
            raise Error('could not get extra channel')

        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            raise Hangup('No inbound call')
        my_log.info("Answered an inbound call")

        # start an outbound call using the extra channel, the destination
        # and origin of the call are given in application_parameters
        # separated by a space. This function will not block.
        destination, origin = application_parameters.split()
        if out_channel.start_call(destination, call_from=origin) is not True:
            raise Error('Outbound not started')

        my_log.info("Outbound call was started")
        # start playing some audio to the inbound call while we wait for the outbound call to connect
        if channel.FilePlayer.start(filename="playfile.wav") is False:
            raise Error("channel failed to start playing a file")

        my_log.info("File play started")
        # wait for the outgoing call to be ANSWERED.
        state = out_channel.wait_for_outgoing_call()
        # stop the audio on the inbound channel
        channel.FilePlayer.stop()
        # check that the outgoing call was answered
        if state != out_channel.State.ANSWERED:
            raise Hangup("Outbound call was not answered")

        # We have an outbound call, let the two
        # parties talk to each other
        if channel.connect(out_channel) is not True:
            raise Error('Connect failed')
        # The calls are connected, the two endpoints can now talk to each other.
        # Wait for the primary channel to go to IDLE
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = 100

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
        if out_channel is not None:
            if out_channel.state() != out_channel.State.IDLE:
                out_channel.hang_up()
    return return_code

Retrievable call transfer

In this example an existing call is transferred to a destination number. Note that the transfer function will require an outbound channel, so the application must have an extra channel available.

Retrievable transfer allows the application to maintain control over the call even after it has been transferred, see the retrievable_transfer function.

The example below shows a call being transferred. The application then waits for the channel to be retrieved. This happens automatically when the far end hangs up, and this is what the application waits for.

The example will also show how to create a tone manager instance and add a custom ring tone to it. It demonstrates creating a tone player on the primary call channel to use the custom tone:

from prosody.uas import Hangup, Error, ToneManager

__uas_identify__ = "application"
__uas_version__  = "0.0.9b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    return_code = 0

    try:
        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            raise Hangup('No inbound call')
        my_log.info("Answered an inbound call")

        # retrievable transfer requires an extra channel
        out_channel = None
        try:
            out_channel = channel.ExtraChannel[0]
        except Exception:
            raise Error("Failed to get the extra channels")

        # for ringback we want to supply a user-defined tone sequence
        # so we need to create a tone manager, this bit is not necessary
        # when using the built-in default tone sequence
        tone_manager = ToneManager(my_log)

        # and now we create the custom tone
        new_tone_sequence = []
        # first tone pair
        new_tone_pair = ({'frequency':400, 'amplitude':10},
                         {'frequency':600, 'amplitude':5},
                         {'duration':3000, 'add':'summation'})
        new_tone_sequence.append(new_tone_pair)
        # second tone pair
        new_tone_pair = ({'frequency':0, 'amplitude':0},
                         {'frequency':0, 'amplitude':0},
                         {'duration':3000, 'add':'summation'})
        new_tone_sequence.append(new_tone_pair)

        # now create the new tone
        if tone_manager.create_tone(new_tone_sequence, 'MY_RING_TONE') is False:
            raise Error('Failed to create the ringtone')

        # set the new tone to be the default
        tone_manager.set_default('MY_RING_TONE')

        # create the channel's tone player, during call transfer the tone player
        # will play the tone manager's default ring tone.
        # this is always required even when not creating a new tone sequence
        channel.create_tone_player(tone_manager)

        # call transfer to a target destination,
        # the destination is given in the application_parameters
        destination = application_parameters

        # retrievable transfer returns the call channel state
        call_state = channel.retrievable_transfer(out_channel, destination)
        # the call state returned should be TRANSFERRED
        if call_state != channel.State.TRANSFERRED:
            # something has gone wrong, we can check the cause
            raise Error("Transfer failed, cause is {0}".format(channel.transfer_cause()))

        # we will now wait until the transfer has ended, either end can hang up
        # if the far end hangs up, the channel state will return to ANSWERED
        if channel.wait_for_transferred_call_retrieval() == channel.State.ANSWERED:
            # we can now handle the channel as an ordinary connected channel
            pass

    except Hangup as exc:
        my_log.info("Got Hangup")
        return_code = 0

    except Error as exc:
        my_log.error("Got Error {0}".format(exc))
        return_code = -101

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

Transfer to a conference room

In this example an existing call is transferred to a conference room. Note that the transfer function will require an outbound channel, so the application must have an extra channel available.

The example will ask the caller to enter a conference room number via DTMF:

from prosody.uas import Hangup, Error, ToneManager

__uas_identify__ = "application"
__uas_version__  = "0.0.9b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    return_code = 0

    try:
        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            raise Hangup('No inbound call')

        # transfer to a conference requires an extra channel
        try:
            out_channel = channel.ExtraChannel[0]
        except:
            raise Error('Failed to get the extra channel')

        channel.FilePlayer.say("Please enter the conference room number followed by the hash key.", barge_in = True)

        digits = channel.DTMFDetector.get_digits(seconds_predigits_timeout = 10, end = "#")

        if channel.DTMFDetector.cause() != channel.DTMFDetector.Cause.END:
            raise Error("we didn't get a valid room name from the user")

        room_name = digits[:-1]
        tts_room_name = ", ".join(list(room_name))
        channel.FilePlayer.say("Joining conference with room name: {0}".format(tts_room_name))

        # because this is a transfer, we need to create the tone player
        tone_manager = ToneManager(my_log)
        channel.create_tone_player(tone_manager)

        # we can set up a number of conferencing options:

        # If the conference party presses *, it will exit the conference room
        # while waiting for the conference to start, the party will hear pleasant music
        conf_media_settings = channel.ConferencePartyMediaSettings(exit_on_dtmf_digit='*',
                                                                   on_conference_stopped_file='pleasant_music.wav')
        # If the party presses 0 it will be muted, 1 it will be unmuted
        conf_media_settings.MuteOnDTMFDigits.set_digits(mute='0', unmute='1')
        # say "george has joined" and "george has left" on entry and exit
        conf_media_settings.PrefixMedia.text_to_say('george')
        conf_media_settings.OnEntryMedia.text_to_say('has joined')
        conf_media_settings.OnExitMedia.text_to_say('has left')
        # the conference will not start automatically when this party enters
        # the conference room will not be destroyed when this party exits (unless no parties are left)
        conf_lifetime_settings = channel.ConferenceLifetimeControl(start_on_entry=False, destroy_on_exit=False)

        if channel.transfer_to_conference_room(out_channel, room_name,
                                          conference_lifetime_control=conf_lifetime_settings,
                                          conference_party_media_settings=conf_media_settings) != channel.State.TRANSFERRED:
            my_log.error("Transfer to conference returned failed, cause is {0}".format(channel.transfer_cause()))
            return_code = -103

        # we can now wait until the party returns from the conference (on pressing *)
        if channel.wait_for_transferred_call_retrieval() == channel.State.ANSWERED:
            # we can now handle the channel as an ordinary connected channel
            pass

    except Hangup as exc:
        my_log.info("Got Hangup")
        return_code = 0

    except Error as exc:
        my_log.error("Got Error {0}".format(exc))
        return_code = -101

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

Connect to a conference room

In this example an existing call is connected to a conference room. Note that this will require an outbound channel, so the application must have an extra channel available.

This examples makes use of the high level API’s call_and_connect_to_conference function.

The example will ask the caller to enter a conference room number via DTMF:

from prosody.uas import Hangup, Error, ToneManager
from prosody.uas.highlevel import HighLevelCallChannel

__uas_identify__ = "application"
__uas_version__  = "0.0.9b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    return_code = 0

    try:
        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            raise Hangup('No inbound call')

        # transfer to a conference requires an extra channel
        try:
            out_channel = channel.ExtraChannel[0]
        except:
            raise Error('Failed to get the extra channel')

        channel.FilePlayer.say("Please enter the conference room number followed by the hash key.", barge_in = True)

        digits = channel.DTMFDetector.get_digits(seconds_predigits_timeout = 10, end = "#")

        if channel.DTMFDetector.cause() != channel.DTMFDetector.Cause.END:
            raise Error("we didn't get a valid room name from the user")

        room_name = digits[:-1]
        tts_room_name = ", ".join(list(room_name))
        channel.FilePlayer.say("Joining conference with room name: {0}".format(tts_room_name))

        # we can set up a number of conferencing options:

        # If the conference party presses *, it will exit the conference room
        # while waiting for the conference to start, the party will hear pleasant music
        conf_media_settings = channel.ConferencePartyMediaSettings(exit_on_dtmf_digit='*',
                                                                   on_conference_stopped_file='pleasant_music.wav')
        # If the party presses 0 it will be muted, 1 it will be unmuted
        conf_media_settings.MuteOnDTMFDigits.set_digits(mute='0', unmute='1')
        # say "george has joined" and "george has left" on entry and exit
        conf_media_settings.PrefixMedia.text_to_say('george')
        conf_media_settings.OnEntryMedia.text_to_say('has joined')
        conf_media_settings.OnExitMedia.text_to_say('has left')
        # the conference will not start automatically when this party enters
        # the conference room will not be destroyed when this party exits (unless no parties are left)
        conf_lifetime_settings = channel.ConferenceLifetimeControl(start_on_entry=False, destroy_on_exit=False)

        high_level_channel = HighLevelCallChannel(channel, my_log)
        if high_level_channel.call_and_connect_to_conference(other_call=out_channel,
                                                             conference_room_name=room_name,
                                                             talker_and_listener=True,
                                                             conference_lifetime_control=conf_lifetime_settings,
                                                             conference_party_media_settings=conf_media_settings) is False:
            raise Error("Transfer to conference failed")

        # wait for other end to go idle (the conference has ended).
        out_channel.wait_for_idle()

    except Hangup as exc:
        my_log.info("Got Hangup")
        return_code = 0

    except Error as exc:
        my_log.error("Got Error {0}".format(exc))
        return_code = -101

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

Calling a local application

Not all calls have to be to a SIP address or telephone number. It is possible, and easy, to call another one of your inbound applications. In this example we demonstrate using call_inbound_service to connect a call channel to another inbound application:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.8b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    return_code = 0
    out_channel = None
    try:
        try:
            out_channel = channel.ExtraChannel[0]
        except:
            raise Error('could not get extra channel')

        if channel.state() == channel.State.CALL_INCOMING:
            channel.ring()   # this can raise a Hangup exception
            channel.answer() # this can raise a Hangup exception
        else:
            raise Hangup('No inbound call')

        my_log.info("Answered an inbound call")

        # place an outbound call using the extra channel, the destination
        # of the call is given in application_parameters.
        # The destination is another inbound application name.

        destination = application_parameters
        if out_channel.call_inbound_service(destination) != channel.State.ANSWERED:
            raise Hangup('No Outbound')

        # We have an outbound call, let the two parties talk to each other
        if channel.connect(out_channel) is True:
            # The calls are connected, the two endpoints can now talk to each other.
            # After some time we may want to disconnect the calls.
            channel.disconnect()
            # it would also be legal to call instead ...
            # out_channel.disconnect()
        else:
            raise Error('Connect failed')

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = 100

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
        if out_channel is not None:
            if out_channel.state() != out_channel.State.IDLE:
                out_channel.hang_up()
    return return_code

Calling a conference room

A call channel that has been transferred or connected (using the connect function) to a conference room cannot be used to play or record media.

If you need to record the conference, or play a message to a conference, then use the call_conference_room` function:

from prosody.uas import Hangup, Error

__uas_identify__ = "application"
__uas_version__  = "0.0.7b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        return_code = 0

        # the conference room name is given in outbound_parameters
        conference_room_name = outbound_parameters
        if channel.call_conference_room(conference_room_name) != channel.State.ANSWERED:
            raise Error("Could not place a call to the conference")

        # now we can play a file to the conference
        channel.FilePlayer.play("my_announcement.wav")

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = 100

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

Fax

An application can be used to send and/or receive a fax on a connected channel, but only if the inbound or outbound service that invokes the application has fax enabled.

Note that any files that are to be sent as a fax must already have been uploaded to cloud.aculab.com. Files can be manually uploaded via the file management page of cloud.aculab.com - while logged in to cloud.aculab.com, click Manage then Media Files. Also have a look at the Web Services API for instructions and examples on automatically uploading files to cloud.aculab.com.

Please note, before working with media files it is important that you also read the section about media files which describes some of the effects of Eventual Consistency.

An application that is going to do some faxing will use a channel that is connected in the usual way, it will then call the send or receive function on the channel’s`` FaxSender`` or FaxReceiver property and supply a fax document object. The fax document object should be ready before the channel has connected the call.

To create a fax document object the application must import FaxToSend and/or FaxToReceive. Below is an example of an outbound application that is going to send a fax:

from prosody.uas import Hangup, Error, FaxToSend, FaxToReceive

__uas_identify__ = "application"
__uas_version__  = "0.01b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        return_code = 0

        # This outbound application is going to send a fax, so we create
        # a FaxToSend object, known as a fax document object
        my_fax = FaxToSend(channel)

        # Now set the content of the fax document object, this is read from
        # a TIFF media file which must be present on cloud.aculab.com
        cause = my_fax.set_content('my_outbound_fax.tif')

        # The cause will be of type FileCause and should be checked.
        if cause != my_fax.FileCause.NORMAL:
            raise Error("failed to set fax content, cause is {0}".format(cause))

        # Place the outbound call in the normal way
        destination = outbound_parameters
        origin = application_parameters
        if channel.call(destination, call_from=origin) != channel.State.ANSWERED:
            cause = channel.cause()
            raise Error("Call destination returned cause {0}".format(cause))

        # Now we can send the fax, we should check the cause to see if the
        # fax was successful
        cause = channel.FaxSender.send(fax_to_send=my_fax)
        if cause == channel.FaxSender.Cause.NORMAL:
            my_log.info("the fax job ended normally")
        elif cause == channel.FaxSender.Cause.NOTFAX:
            raise Hangup("the receiver was not a fax machine")
        else:
            raise Error("fax send failed with cause {0}".format(cause))

        # In faxing, it is the receiver that hangs up first, so it is polite
        # for the fax sender to wait for the other end to hang up.
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = -101

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

To receive an inbound fax is very similar:

from prosody.uas import Hangup, Error, FaxToSend, FaxToReceive

__uas_identify__ = "application"
__uas_version__  = "0.01b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    try:
        return_code = 0

        # This outbound application is going to receive a fax, so we create
        # a FaxToReceive object, known as a fax document object
        my_fax = FaxToReceive(channel)

        # Now set the name of the file to which the fax will be saved.
        cause = my_fax.set_file_name('my_inbound_fax.tif')

        # The cause will be of type FileCause and should be checked.
        if cause != my_fax.FileCause.NORMAL:
            raise Error("failed to set the file name, cause is {0}".format(cause))

        # Answer the inbound call in the normal way
        if channel.state() == channel.State.CALL_INCOMING:
            channel.answer() # this can raise a Hangup exception
        else:
            raise Hangup('No inbound call')

        # Now we can receive the fax, we should check the cause to see if the
        # fax was successful.
        cause = channel.FaxReceiver.receive(fax_to_receive=my_fax)
        if cause == channel.FaxReceiver.Cause.NORMAL:
            my_log.info("the fax job ended normally")
        elif cause == channel.FaxReceiver.Cause.NOTFAX:
            raise Hangup("the transmitter was not a fax machine")
        else:
            raise Error("fax receive failed with cause {0}".format(cause))

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = -101

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

The FaxToSend and FaxToReceive objects provide a number of useful functions that help to track the progress of a fax session. This is particularly useful when sending or receiving a fax that has many pages. Below is an example of a fax sender that will print a progress report while the fax is progressing:

from prosody.uas import Hangup, Error, FaxToSend, FaxToReceive

__uas_identify__ = "application"
__uas_version__  = "0.01b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        return_code = 0

        # This outbound application is going to send a fax, so we create
        # a FaxToSend object, known as a fax document object
        my_fax = FaxToSend(channel)

        # Now set the content of the fax document object, this is read from
        # a TIFF media file which must be present on cloud.aculab.com
        cause = my_fax.set_content('my_outbound_fax.tif')

        # The cause will be of type FileCause and should be checked.
        if cause != my_fax.FileCause.NORMAL:
            raise Error("failed to set fax content, cause is {0}".format(cause))

        # Place the outbound call in the normal way
        destination = outbound_parameters
        origin = application_parameters
        if channel.call(destination, call_from=origin) != channel.State.ANSWERED:
            cause = channel.cause()
            raise Error("Call destination returned cause {0}".format(cause))

        # Now we can start sending the fax.
        if channel.FaxSender.start(fax_to_send=my_fax) is not True:
            cause = channel.FaxSender.cause()
            raise Error("fax sender failed to start sending the fax, cause is {0}.".format(cause))

        # The fax endpoints will negotiate the parameters for the fax session - which modem
        # and the modem speed, among other things. We can wait until these have been published
        # and then have a look. Note that fax negotiation can fail, in which case the dictionary
        # will be empty.
        negotiated_settings = channel.FaxSender.wait_for_negotiated_settings()
        if negotiated_settings == {}:
            # negotiation has failed, we should quit and hang up.
            cause = channel.FaxSender.cause()
            if cause == channel.FaxSender.Cause.NOTFAX:
                raise Hangup("the receiver was not a fax machine")
            else:
                raise Error("Fax negotiation failed with cause {0}".format(cause))

        print("Negotiated Settings")
        for setting, value in negotiated_settings.iteritems():
            print("{0:>15.15} : {1}".format(setting, value))

        # Now we can wait for each page to be sent, we do this until we've been told
        # that no more pages will be sent. It is important to note that the last_page
        # flag does not indicate success, it simply means that no more pages will be processed.
        while channel.FaxSender.Details.last_page is not True:
            pages = channel.FaxSender.wait_for_next_page()
            print("Sent {0} pages so far".format(pages))

        # Now we need to wait until the fax session has finished. We need to do this even though
        # the last page flag has been set. Remember to check the cause, the last_page indicator
        # may have been set, but this does not mean that every page was sent.
        cause = channel.FaxSender.wait_until_finished()
        if cause != channel.FaxSender.Cause.NORMAL:
            raise Error("The fax sender failed with cause {0}".format(channel.FaxSender.Details.raw_cause))

        # In faxing, it is the receiver that hangs up first, so it is polite
        # for the fax sender to wait for the other end to hang up.
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = -101

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

A fax receiver can similarly report on the progress of the fax session, it can also give extra information on each page as it is received:

from prosody.uas import Hangup, Error, FaxToSend, FaxToReceive

__uas_identify__ = "application"
__uas_version__  = "0.01b"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    try:
        return_code = 0

        # This outbound application is going to receive a fax, so we create
        # a FaxToReceive object, known as a fax document object
        my_fax = FaxToReceive(channel)

        # Now set the name of the file to which the fax will be saved.
        cause = my_fax.set_file_name('my_inbound_fax.tif')

        # The cause will be of type FileCause and should be checked.
        if cause != my_fax.FileCause.NORMAL:
            raise Error("failed to set the file name, cause is {0}".format(cause))

        # Answer the inbound call in the normal way
        if channel.state() == channel.State.CALL_INCOMING:
            channel.answer() # this can raise a Hangup exception
        else:
            raise Hangup('No inbound call')

        # Now we can start to receive the fax.
        if channel.FaxReceiver.start(fax_to_receive=my_fax) != True:
            cause = channel.FaxReceiver.cause()
            raise Error("fax receiver failed to start receiving the fax, cause is {0}.".format(cause))

        # The fax endpoints will negotiate the parameters for the fax session - which modem
        # and the modem speed, among other things. We can wait until these have been published
        # and then have a look. Note that fax negotiation can fail, in which case the dictionary
        # will be empty.
        negotiated_settings = channel.FaxReceiver.wait_for_negotiated_settings()
        if negotiated_settings == {}:
            # negotiation has failed, we should quit and hang up.
            cause = channel.FaxReceiver.cause()
            if cause == channel.FaxReceiver.Cause.NOTFAX:
                raise Hangup("the transmitter was not a fax machine")
            else:
                raise Error("Fax negotiation failed with cause {0}".format(cause))

        print("Negotiated Settings")
        for setting, value in negotiated_settings.iteritems():
            print("{0:>15.15} : {1}".format(setting, value))

        # Now we can wait for each page to be received, we do this until we've been told
        # that no more pages will be received. For each page we print the page quality.
        # This, and more, detail is available on the fax document object.
        # It is important to note that the last_page flag does not indicate success, it simply
        # means that no more pages will be processed.
        while channel.FaxReceiver.Details.last_page is not True:
            pages = channel.FaxReceiver.wait_for_next_page()
            print("Received {0} pages so far".format(pages))
            print("Last received page quality: {0}".format(my_fax.PageDetails[-1].receive_quality))

        # Now we need to wait until the fax session has finished. We need to do this even though
        # the last page flag has been set. Remember to check the cause, the last_page indicator
        # may have been set, but this does not mean that every page was received.
        cause = channel.FaxReceiver.wait_until_finished()
        if cause != channel.FaxReceiver.Cause.NORMAL:
            raise Error("Fax receiver failed with cause {0}".format(channel.FaxReceiver.Details.raw_cause))

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = -101

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

Note that the wait functions (wait_for_negotiated_settings, wait_for_next_page and wait_until_finished) in the examples above can throw a Hangup or Error exception.

The FaxToSend object also provides the ability to modify the contents of the fax document object after its contents have been set. For instance, to insert a line of text, or to append pages.

In the example below we do both:

from prosody.uas import Hangup, Error, FaxToSend, FaxToReceive

__uas_identify__ = "application"
__uas_version__  = "0.01b"

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):
    try:
        return_code = 0

        # This outbound application is going to send a fax, so we create
        # a FaxToSend object, known as a fax document object
        my_fax = FaxToSend(channel)

        # Now set the content of the fax document object, this is read from
        # a TIFF media file which must be present on cloud.aculab.com. In this case
        # the content is a fax header, to which more content will be added.
        cause = my_fax.set_content('my_fax_header.tif')

        # The cause will be of type FileCause and should be checked.
        if cause != my_fax.FileCause.NORMAL:
            raise Error("failed to set fax content, cause is {0}".format(cause))

        # Add the main fax body to the document.
        cause = my_fax.append('my_outbound_fax.tif')
        if cause != my_fax.FileCause.NORMAL:
            raise Error("failed to set fax content, cause is {0}".format(cause))

        # Add the page number to each page of the fax document.
        # First create a PageText object.
        page_text = my_fax.create_page_text()

        # We want the page number to be one inch from the top of the page.
        # So we set the unit of measurement to inches.
        page_text.Position.unit = page_text.Position.PagePositionUnits.INCHES
        page_text.Position.from_page_top = 1

        # We want the existing line at our pgae number position to be completely removed
        page_text.mode = page_text.PageTextMode.REPLACE

        # On each page put the page number on the left
        for i in range(my_fax.pages_to_send):
            page_text.left_text = 'This is page {0}'.format(i + 1)
            # page numbers start at 1
            cause = my_fax.add_text_to_page(i + 1, page_text)
            if cause != my_fax.FileCause.NORMAL:
                raise Error("failed to set fax content, cause is {0}".format(cause))

        # Place the outbound call in the normal way
        destination = outbound_parameters
        origin = application_parameters
        if channel.call(destination, call_from=origin) != channel.State.ANSWERED:
            cause = channel.cause()
            raise Error("Call destination returned cause {0}".format(cause))

        # Now we can start sending the fax.
        if channel.FaxSender.start(fax_to_send=my_fax) is not True:
            cause = channel.FaxSender.cause()
            raise Error("fax sender failed to start sending the fax, cause is {0}.".format(cause))

        # The fax endpoints will negotiate the parameters for the fax session - which modem
        # and the modem speed, among other things. We can wait until these have been published
        # and then have a look. Note that fax negotiation can fail, in which case the dictionary
        # will be empty.
        negotiated_settings = channel.FaxSender.wait_for_negotiated_settings()
        if negotiated_settings == {}:
            # negotiation has failed, we should quit and hang up.
            cause = channel.FaxSender.cause()
            if cause == channel.FaxSender.Cause.NOTFAX:
                raise Hangup("the receiver was not a fax machine")
            else:
                raise Error("Fax negotiation failed with cause {0}".format(cause))

        print("Negotiated Settings")
        for setting, value in negotiated_settings.iteritems():
            print("{0:>15.15} : {1}".format(setting, value))

        # Now we can wait for each page to be sent, we do this until we've been told
        # that no more pages will be sent.
        while channel.FaxSender.Details.last_page is not True:
            pages = channel.FaxSender.wait_for_next_page()
            print("Sent {0} pages so far".format(pages))

        # Now we wait until the fax session has finished.
        cause = channel.FaxSender.wait_until_finished()
        if cause != channel.FaxSender.Cause.NORMAL:
            raise Error("The fax sender failed with cause {0}".format(channel.FaxSender.Details.raw_cause))

        # In faxing it is the receiver that hangs up first, so it is polite
        # for the fax sender to wait for the other end to hang up.
        channel.wait_for_idle()

    except Hangup as exc:
        my_log.info("Hangup exception reports {0}".format(exc))
        return_code = -101

    except Error as exc:
        my_log.error("Error exception reports {0}".format(exc))
        return_code = -100

    except Exception as exc:
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -102

    finally:
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
    return return_code

Multiple file applications

When applications become more complex, it is quite likely that you will want them to use shared code. Shared code is put in common files, you can read up on this here.

A file that contains common code for making calls might look like this:

from prosody.uas import Hangup, Error
import time

__uas_version__  = "0.0.1"
__uas_identify__ = "common"

def place_call(channel, destination):
    # Place an outbound call. If the destination is BUSY,
    # wait ten seconds and try again; try for one minute.
    endTime = time.time() + 60

    while channel.call(destination) != channel.State.ANSWERED:
        cause = channel.cause()
        if cause != channel.Cause.BUSY:
            raise Error("Call destination returned cause {0}".format(cause))
        if time.time() > endTime:
            raise Hangup("Call destination is busy.")
        time.sleep(10)
        continue

Further reading

For further code samples please see the example code. Also, have a look at the online documentation on cloud.aculab.com.