1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
#!/usr/bin/env python
# vim: sw=3 et:
"""Module that provides classes to track a ConfBridge test.
This module has been superceded by the apptest module.
Copyright (C) 2011, Digium, Inc.
Matt Jordan <mjordan@digium.com>
This program is free software, distributed under the terms of
the GNU General Public License Version 2.
"""
import sys
import logging
from test_state import TestState
from twisted.internet import reactor
sys.path.append("lib/python")
LOGGER = logging.getLogger(__name__)
class ConfbridgeChannelObject(object):
"""A tracking object that ties together information about the channels
involved in a ConfBridge
"""
def __init__(self, conf_bridge_channel, caller_channel, caller_ami,
profile_option=""):
"""Constructor
Keyword Arguments:
conf_bridge_channel The channel inside the Asterisk instance hosting
the ConfBridge
caller_channel The channel inside the Asterisk instance that
called the ConfBridge server
caller_ami An AMI connection back to the calling Asterisk
instance
profile_option Some string field that identifies the profile set
for the conf_bridge_channel
"""
self.conf_bridge_channel = conf_bridge_channel
self.caller_channel = caller_channel
self.caller_ami = caller_ami
self.profile = profile_option
class ConfbridgeTestState(TestState):
"""Base class test state for ConfBridge. Allows states to send DTMF tones,
audio files, hangup, and otherwise interact with the conference. As this
inherits from test_state, this is also an entry in the state engine, such
that it will receive test event notifications. Derived classes should handle
these state notifications, and use the methods in this class to respond
accordingly.
"""
def __init__(self, controller, test_case, calls=None):
"""Constructor
Keyword Arguments:
controller The TestStateController managing the test
test_case The main test object
calls A dictionary (keyed by conf_bridge_channel ID) of
ConfbridgeChannelObjects
"""
TestState.__init__(self, controller)
self.test_case = test_case
self.calls = calls or {}
self.__previous_dtmf = {}
self.__previous_audio = {}
if (len(self.calls) > 0):
for key in self.calls:
self.__previous_dtmf[key] = ""
self.__previous_audio[key] = ""
LOGGER.debug(" Entering state [" + self.get_state_name() + "]")
def register_new_caller(self, channel_object):
"""Register a new ConfbridgeChannelObject with the state engine
Keyword Arguments:
channel_object An object that ties all of the various channels/AMI
information together
"""
if not (channel_object.conf_bridge_channel in self.calls):
self.calls[channel_object.conf_bridge_channel] = channel_object
self.__previous_dtmf[channel_object.conf_bridge_channel] = ""
self.__previous_audio[channel_object.conf_bridge_channel] = ""
def get_state_name(self):
"""Should be overriden by derived classes and return the name of the
current state
"""
pass
def handle_default_state(self, event):
"""Can be called by derived classes to output a state that is being
ignored
"""
msg = ("State [" + self.get_state_name() +
"] - ignoring state change " + event['state'])
LOGGER.debug(msg)
def _handle_redirect_failure(self, reason):
"""Handle a redirect failure from Asterisk"""
LOGGER.warn("Error sending redirect - test may or may not fail:")
LOGGER.warn(reason.getTraceback())
return reason
def hangup(self, call_id):
"""Hangs up the current call
Keyword Arguments:
call_id The channel name
"""
if call_id in self.calls:
chan = self.calls[call_id].caller_channel
ami = self.calls[call_id].caller_ami
deferred = ami.redirect(chan, "caller", "hangup", 1)
deferred.addErrback(self._handle_redirect_failure)
else:
LOGGER.warn("Unknown call ID %s" % call_id)
def send_dtmf(self, call_id, dtmf):
"""Send a DTMF signal to the server
Keyword Arguments:
call_id The channel name, from the perspective of the ConfBridge app
dtmf The DTMF code to send
"""
if call_id in self.calls:
LOGGER.debug("Attempting to send DTMF %s via %s" % (dtmf, call_id))
ami = self.calls[call_id].caller_ami
if (self.__previous_dtmf[call_id] != dtmf):
ami.setVar(channel=self.calls[call_id].caller_channel,
variable="DTMF_TO_SEND",
value=dtmf)
self.__previous_dtmf[call_id] = dtmf
# Redirect to the DTMF extension - note that we assume that we only
# have one channel to the other asterisk instance
deferred = ami.redirect(self.calls[call_id].caller_channel,
"caller", "sendDTMF", 1)
deferred.addErrback(self._handle_redirect_failure)
else:
LOGGER.warn("Unknown call ID %s" % call_id)
def send_sound_file(self, call_id, audio_file):
"""Send a sound file to the server
Keyword Arguments:
call_id The channel name, from the perspective of the ConfBridge app
audio_file The local path to the file to stream
"""
if call_id in self.calls:
LOGGER.debug("Attempting to send audio file %s via %s" %
(audio_file, call_id))
ami = self.calls[call_id].caller_ami
if (self.__previous_audio[call_id] != audio_file):
ami.setVar(channel=self.calls[call_id].caller_channel,
variable="TALK_AUDIO",
value=audio_file)
self.__previous_audio[call_id] = audio_file
# Redirect to the send sound file extension - note that we assume
# that we only have one channel to the other asterisk instance
deferred = ami.redirect(self.calls[call_id].caller_channel,
"caller", "sendAudio", 1)
deferred.addErrback(self._handle_redirect_failure)
else:
LOGGER.warn("Unknown call ID %s" % call_id)
def send_sound_file_with_dtmf(self, call_id, audio_file, dtmf):
"""Send a sound file to the server, then send a DTMF signal
Keyword Arguments:
call_id The channel name, from the perspective of the ConfBridge app
audio_file The local path to the file to stream
dtmf The DTMF signal to send
Note that this is necessary so that when the audio file is finished, we
close the audio recording cleanly; otherwise, Asterisk may detect the
end of file as a hangup
"""
if call_id in self.calls:
msg = ("Attempting to send audio file %s followed by %s via %s" %
(audio_file, dtmf, call_id))
LOGGER.debug(msg)
ami = self.calls[call_id].caller_ami
if (self.__previous_audio[call_id] != audio_file):
ami.setVar(channel=self.calls[call_id].caller_channel,
variable="TALK_AUDIO",
value=audio_file)
self.__previous_audio[call_id] = audio_file
if (self.__previous_dtmf[call_id] != dtmf):
ami.setVar(channel=self.calls[call_id].caller_channel,
variable="DTMF_TO_SEND",
value=dtmf)
self.__previous_dtmf[call_id] = dtmf
# Redirect to the send sound file extension - note that we assume
# that we only have one channel to the other asterisk instance
deferred = ami.redirect(self.calls[call_id].caller_channel,
"caller", "sendAudioWithDTMF", 1)
deferred.addErrback(self._handle_redirect_failure)
else:
LOGGER.warn("Unknown call ID %s" % call_id)
def schedule_dtmf(self, delay, call_id, dtmf):
"""Schedule and send a DTMF tone sometime later
Keyword Arguments:
delay The number of seconds to wait
call_id The channel name, from the perspective of the ConfBridge app
dtmf The DTMF code to send
"""
reactor.callLater(delay, self.send_dtmf, call_id, dtmf)
def schedule_sound_file(self, delay, call_id, audio_file):
"""Schedule and send an audio file
Keyword Arguments:
delay The number of seconds to wait
call_id The channel name, from the perspective of the ConfBridge app
audio_file The local path to the file to stream
"""
reactor.callLater(delay, self.send_sound_file, call_id, audio_file)
|