class AMISpecifier(propertied.Propertied): """Manager interface setup/specifier""" username = common.StringLocaleProperty( "username", """Login username for the manager interface""", ) secret = common.StringLocaleProperty( "secret", """Login secret for the manager interface""", ) password = secret server = common.StringLocaleProperty( "server", """Server IP address to which to connect""", defaultValue='127.0.0.1', ) port = common.IntegerProperty( "port", """Server IP port to which to connect""", defaultValue=5038, ) timeout = common.FloatProperty( "timeout", """Timeout in seconds for an AMI connection timeout""", defaultValue=5.0, ) def login(self): """Login to the specified manager via the AMI""" theManager = manager.AMIFactory(self.username, self.secret) return theManager.login(self.server, self.port, timeout=self.timeout)
class CollectPassword(CollectDigits): """Collects some number of password digits from the user""" runnerClass = CollectPasswordRunner escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from password entry""", defaultValue = '', ) soundFile = common.StringLocaleProperty( "soundFile", """File (name) for the pre-recorded blurb""", defaultValue = 'vm-password', )
class CollectDigits(Interaction): """Collects some number of digits (e.g. an extension) from user""" soundFile = common.StringLocaleProperty( "soundFile", """File (name) for the pre-recorded blurb""", ) textPrompt = common.StringProperty( "textPrompt", """Textual prompt describing the option""", ) readBack = common.BooleanProperty( "readBack", """Whether to read the entered value back to the user""", defaultValue=False, ) minDigits = common.IntegerProperty( "minDigits", """Minimum number of digits to collect (only restricted if specified)""", ) maxDigits = common.IntegerProperty( "maxDigits", """Maximum number of digits to collect (only restricted if specified)""", ) runnerClass = CollectDigitsRunner tellInvalid = common.IntegerProperty( "tellInvalid", """Whether to tell the user that their selection is unrecognised""", defaultValue=True, )
class CollectAudio(Interaction): """Collects audio file from the user""" prompt = common.ListProperty( "prompt", """(Set of) prompts to run, can be Prompt instances or filenames Used by the PromptRunner to produce prompt selections """, ) textPrompt = common.StringProperty( "textPrompt", """Textual prompt describing the option""", ) temporaryFile = common.StringLocaleProperty( "temporaryFile", """Temporary file into which to record the audio before moving to filename""", ) filename = common.StringLocaleProperty( "filename", """Final filename into which to record the file...""", ) deleteOnFail = common.BooleanProperty( "deleteOnFail", """Whether to delete failed attempts to record a file""", defaultValue=True) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from recording the file""", defaultValue='#*0123456789', ) timeout = common.FloatProperty( "timeout", """Duration to wait for recording (maximum record time)""", defaultValue=60, ) silence = common.FloatProperty( "silence", """Duration to wait for recording (maximum record time)""", defaultValue=5, ) beep = common.BooleanProperty( "beep", """Whether to play a "beep" sound at beginning of recording""", defaultValue=True, ) runnerClass = CollectAudioRunner
class AGISpecifier(propertied.Propertied): """Specifier of where we send the user to connect to our AGI""" port = common.IntegerProperty( "port", """IP port on which to listen""", defaultValue=4573, ) interface = common.StringLocaleProperty( "interface", """IP interface on which to listen (local only by default)""", defaultValue='127.0.0.1', ) context = common.StringLocaleProperty( "context", """Asterisk context to which to connect incoming calls""", defaultValue='survey', ) def run(self, mainFunction): """Start up the AGI server with the given mainFunction""" f = fastagi.FastAGIFactory(mainFunction) return reactor.listenTCP(self.port, f, 50, self.interface)
class CollectPasswordRunner(CollectDigitsRunner): """Password-runner, checks validity versus expected value""" expected = common.StringLocaleProperty( "expected", """The value expected/required from the user for this run""", ) def __call__(self, expected, *args, **named): """Begin the AGI processing for the menu""" self.expected = expected return super(CollectPasswordRunner, self).__call__(*args, **named) def validEntry(self, digits): """Determine whether given digits are considered a "valid" entry""" for digit in self.model.escapeDigits: if digit in digits: raise error.MenuExit( self.model, """User cancelled entry of password""", ) if digits != self.expected: return False, "Password doesn't match" return True, None
class PromptRunner(propertied.Propertied): """Prompt formed from list of sub-prompts """ elements = common.ListProperty( "elements", """Sub-elements of the prompt to be presented""", ) agi = basic.BasicProperty( "agi", """The FastAGI instance we're controlling""", ) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from playing the prompt""", ) timeout = common.FloatProperty( "timeout", """Timeout on data-entry after completed reading""", ) def __call__(self): """Return a deferred that chains all of the sub-prompts in order Returns from the first of the sub-prompts that recevies a selection returns str(digit) for the key the user pressed """ return self.onNext(None) def onNext(self, result, index=0): """Process the next operation""" if result is not None: return result try: element = self.elements[index] except IndexError, err: # okay, do a waitForDigit from timeout seconds... return self.agi.waitForDigit(self.timeout).addCallback( self.processKey).addCallback(self.processLast) else:
class Option(propertied.Propertied): """A single menu option that can be chosen by the user""" option = common.StringLocaleProperty( "option", """Keypad values which select this option (list of characters)""", )
class MenuRunner(Runner): """User's single interaction with a given menu""" def defaultEscapeDigits(prop, client): """Return the default escape digits for the given client""" if client.model.tellInvalid: escapeDigits = client.model.ALL_DIGITS else: escapeDigits = "".join([o.option for o in client.model.options]) return escapeDigits escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from prompts to choose option""", defaultFunction = defaultEscapeDigits, ) del defaultEscapeDigits # clean up namespace def __call__(self, *args, **named): """Begin the AGI processing for the menu""" self.readMenu() return self.finalDF def readMenu(self, result=None): """Read our menu to the user""" runner = self.promptAsRunner(self.model.prompt) return runner().addCallback(self.onReadMenu).addErrback(self.returnError) def onReadMenu(self, pressed): """Deal with succesful result from reading menu""" log.info("""onReadMenu: %r""", pressed) if not pressed: self.alreadyRepeated += 1 if self.alreadyRepeated >= self.model.maxRepetitions: log.warn("""User did not complete menu selection for %s, timing out""", self.model) if not self.finalDF.called: raise error.MenuTimeout( self.model, """User did not finish selection in %s passes of menu""" % ( self.alreadyRepeated, ) ) return None return self.readMenu() else: # Yay, we got an escape-key pressed for option in self.model.options: if pressed in option.option: if callable(option): # allow for chaining down into sub-menus and the like... # we return the result of calling the option via self.finalDF return defer.maybeDeferred(option, pressed, self).addCallbacks( self.returnResult, self.returnError ) elif hasattr(option, 'onSuccess'): return defer.maybeDeferred(option.onSuccess, pressed, self).addCallbacks( self.returnResult, self.returnError ) else: return self.returnResult([(option,pressed),]) # but it wasn't anything we expected... if not self.model.tellInvalid: raise error.MenuUnexpectedOption( self.model, """User somehow selected %r, which isn't a recognised option?""" % (pressed,), ) else: return self.agi.getOption( self.model.INVALID_OPTION_FILE, self.escapeDigits, timeout=0, ).addCallback(self.readMenu).addErrback(self.returnError)
class CollectAudioRunner(Runner): """Audio-collection runner, records user audio to a file on the asterisk server""" escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from recording""", defaultFunction = lambda prop, client: client.model.escapeDigits, setDefaultOnGet = False, ) def __call__(self, *args, **named): """Begin the AGI processing for the menu""" self.readPrompt() return self.finalDF def readPrompt(self, result=None): """Begin process of reading audio from the user""" if self.model.prompt: # wants us to read a prompt to the user before recording... runner = self.promptAsRunner(self.model.prompt) runner.timeout = 0.1 return runner().addCallback(self.onReadPrompt).addErrback(self.returnError) else: return self.collectAudio().addErrback(self.returnError) def onReadPrompt(self, result): """We've finished reading the prompt to the user, check for escape""" log.info('Finished reading prompt for collect audio: %r', result) if result and result in self.escapeDigits: raise error.MenuExit( self.model, """User cancelled entry of audio during prompt""", ) else: return self.collectAudio() def collectAudio( self ): """We're supposed to record audio from the user with our model's parameters""" # XXX use a temporary file for recording the audio, then move to final destination log.debug('collectAudio') if hasattr(self.model, 'temporaryFile'): filename = self.model.temporaryFile else: filename = self.model.filename df = self.agi.recordFile( filename=filename, format=self.model.format, escapeDigits=self.escapeDigits, timeout=self.model.timeout, offsetSamples=None, beep=self.model.beep, silence=self.model.silence, ).addCallbacks( self.onAudioCollected, self.onAudioCollectFail, ) if hasattr(self.model, 'temporaryFile'): df.addCallback(self.moveToFinal) return df def onAudioCollected(self, result): """Process the results of collecting the audio""" digits, typeOfExit, endpos = result if typeOfExit in ('hangup', 'timeout'): # expected common-case for recording... return self.returnResult((self,(digits,typeOfExit,endpos))) elif typeOfExit =='dtmf': raise error.MenuExit( self.model, """User cancelled entry of audio""", ) else: raise ValueError("""Unrecognised recordFile results: (%s, %s %s)""" % ( digits, typeOfExit, endpos, )) def onAudioCollectFail(self, reason): """Process failure to record audio""" log.error( """Failure collecting audio for CollectAudio instance %s: %s""", self.model, reason.getTraceback(), ) return reason # re-raise the error... def moveToFinal(self, result): """On succesful recording, move temporaryFile to final file""" log.info( 'Moving recorded audio %r to final destination %r', self.model.temporaryFile, self.model.filename ) import os try: os.rename( '%s.%s' % (self.model.temporaryFile, self.model.format), '%s.%s' % (self.model.filename, self.model.format), ) except (OSError, IOError), err: log.error( """Unable to move temporary recording file %r to target file %r: %s""", self.model.temporaryFile, self.model.filename, # XXX would like to use getException here... err, ) raise return result
class Argument(propertied.Propertied): """Representation of a single argument on a callable object""" name = common.StringLocaleProperty( 'name', """The argument's name, as a simple string""", ) default = basic.BasicProperty( 'default', """Default-value for the argument, may be NULL/unavailable""", ) baseType = basic.BasicProperty( 'baseType', """Base data-type for the argument, may be NULL/unavailable""", ) def __init__(self, name, default=__NULL__, baseType=__NULL__, **named): """Initialize the Callable object name -- the argument name default -- if provided, will provide the default value for the argument baseType -- if provided, will allow for type checking and coercion of arguments before calling the callable object. """ if default is not __NULL__: named["default"] = default if baseType is not __NULL__: named["baseType"] = baseType super(Argument, self).__init__(name=name, **named) def __str__(self, ): """Create a friendly string representation""" fragments = [repr(self.name)] if hasattr(self, "default"): fragments.append(repr(self.default)) if hasattr(self, "baseType"): fragments.append(repr(self.baseType)) return """%s(%s)""" % ( self.__class__.__name__, ", ".join(fragments), ) __repr__ = __str__ def __eq__(self, other): """Determine whether other is our equivalent returns true if other is of the same class, with the same primary attributes """ if self.__class__ is not other.__class__: return 0 NULL = [] for nm in ['name', 'default', 'baseType']: if hasattr(self, nm) and not hasattr(other, nm): return 0 elif not hasattr(self, nm) and hasattr(other, nm): return 0 elif hasattr(self, nm): if getattr(self, nm) != getattr(other, nm): return 0 return 1 ### Data-type API def check(cls, value): """Strict check to see if value is an instance of cls""" return isinstance(value, cls) check = classmethod(check) def coerce(cls, value): """Coerce value to a cls instance Accepted forms: ("name",) ("name",default) ("name",default,baseType) "name" { ** } # passed to the initialiser """ if cls.check(value): return value if isinstance(value, (tuple, list)) and value and len(value) < 4: items = {} for item, name in zip(value, ['name', 'default', 'baseType'][:len(value)]): items[name] = item return cls(**items) elif isinstance(value, str): return cls(name=value) elif isinstance(value, dict): return cls(**value) raise TypeError("""Don't know how to convert %r to a %s object""" % (value, cls.__name__)) coerce = classmethod(coerce)
class PromptRunner(propertied.Propertied): """Prompt formed from list of sub-prompts """ elements = common.ListProperty( "elements", """Sub-elements of the prompt to be presented""", ) agi = basic.BasicProperty( "agi", """The FastAGI instance we're controlling""", ) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from playing the prompt""", ) timeout = common.FloatProperty( "timeout", """Timeout on data-entry after completed reading""", ) def __call__(self): """Return a deferred that chains all of the sub-prompts in order Returns from the first of the sub-prompts that recevies a selection returns str(digit) for the key the user pressed """ return self.onNext(None) def onNext(self, result, index=0): """Process the next operation""" if result is not None: return result try: element = self.elements[index] except IndexError as err: # okay, do a waitForDigit from timeout seconds... return self.agi.waitForDigit(self.timeout).addCallback( self.processKey).addCallback(self.processLast) else: df = element.read(self.agi, self.escapeDigits) df.addCallback(self.processKey) df.addCallback(self.onNext, index=index + 1) return df def processKey(self, result): """Does the pressed key belong to escapeDigits?""" if isinstance(result, tuple): # getOption result... if result[1] == 0: # failure during load of the file... log.warn("Apparent failure during load of audio file: %s", self.value) result = 0 else: result = result[0] if isinstance(result, str): if result: result = ord(result) else: result = 0 if result: # None or 0 # User pressed a key during the reading... key = chr(result) if key in self.escapeDigits: log.info('Exiting early due to user press of: %r', key) return key else: # we don't warn user in this menu if they press an unrecognised key! log.info( 'Ignoring user keypress because not in escapeDigits: %r', key) # completed reading without any escape digits, continue reading return None def processLast(self, result): if result is None: result = '' return result