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 the Aculab Cloud.

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 the Aculab Cloud, 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 the Aculab Cloud, it must first be registered against a service on the Aculab Cloud Web Portal. 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 the Aculab Cloud. 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.

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")

The are a number of rules that go with making outbound PSTN calls. Please read the information on the Aculab Cloud website.

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

Media recording and playback

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

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 located in a pre-configured directory.

The two functions described above will block until the recording or playback is complete, this is not always desireable 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

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. 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

File handling

The API offers some basic file and directory handling functions for the cloud based file system. 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.

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 exisitng 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

Calling a local application

Not all calls have to be to a SIP or PSTN destination. It is possible, and easy, to call another one of your inbound cloud applications. In this example we demonstrate using call_inbound_service to connect a call channel to another inbound application. Similarly, a call channel can be transferred to an 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 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

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.