starpy-1.0.1.0.git.20151124/000077500000000000000000000000001274025253300146545ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/.travis.yml000066400000000000000000000001271274025253300167650ustar00rootroot00000000000000language: python python: - "2.6" - "2.7" - "3.2" script: python setup.py install starpy-1.0.1.0.git.20151124/LICENSE000066400000000000000000000031031274025253300156560ustar00rootroot00000000000000Copyright (c) 2006, Michael C. Fletcher All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of Michael C. Fletcher, or the name of any Contributor, may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY SITUATION ENDANGERING HUMAN LIFE OR PROPERTY. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. starpy-1.0.1.0.git.20151124/MANIFEST.in000066400000000000000000000001271274025253300164120ustar00rootroot00000000000000include *.txt include LICENSE include README.rst recursive-include examples *.txt *.py starpy-1.0.1.0.git.20151124/README.rst000066400000000000000000000016731274025253300163520ustar00rootroot00000000000000StarPy Asterisk Protocols for Twisted ===================================== StarPy is a Python + Twisted protocol that provides access to the Asterisk PBX's Manager Interface (AMI) and Fast Asterisk Gateway Interface (FastAGI). Together these allow you write both command-and-control interfaces (used, for example to generate new calls) and to customise user interactions from the dialplan. You can readily write applications that use the AMI and FastAGI protocol together with any of the already available Twisted protocols. StarPy is primarily intended to allow Twisted developers to add Asterisk connectivity to their Twisted applications. It isn't really targeted at the normal AGI-writing populace, as it requires understanding Twisted's asynchronous programming model. That said, if you do know Twisted, it can readily be used to write stand-alone FastAGIs. StarPy is Open Source and we are interested in contributions, bug reports and feedback. starpy-1.0.1.0.git.20151124/doc/000077500000000000000000000000001274025253300154215ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/doc/CHANGES.txt000066400000000000000000000011551274025253300172340ustar00rootroot00000000000000CHANGE notes for starpy python module ====================================== - References to zaptel functions have been converted into their DAHDI equivalents. For most of the functions, the first three letters of the old function names "zap", have been removed and replaced with "dahdi". Old Zaptel Func. | New Zaptel Func. ====================================== zapDNDon | dahdiDNDon zapDNDoff | dahdiDNDoff zapDialOffHook | dahdiDialOffHook zapHangup | dahdiHangup zapshowchannels | dahdiShowChannels zaptransfers | dahdiTransfers starpy-1.0.1.0.git.20151124/doc/UPGRADE.txt000066400000000000000000000005711274025253300172540ustar00rootroot00000000000000UPGRADE notes for starpy python module ====================================== - References to zaptel functions have been converted into their DAHDI equivalents. For most of the functions, the first three letters of the old function names "zap", have been removed and replaced with "dahdi". i.e. zapDNDoff is now dahdiDNDoff See CHANGES.txt for a complete list. starpy-1.0.1.0.git.20151124/doc/index.html000066400000000000000000000566431274025253300174340ustar00rootroot00000000000000 StarPy Asterisk Protocols for Twisted

StarPy Asterisk Protocols for Twisted

StarPy is a Python + Twisted protocol that provides access to the Asterisk PBX's Manager Interface (AMI) and Fast Asterisk Gateway Interface (FastAGI). Together these allow you write both command-and-control interfaces (used, for example to generate new calls) and to customise user interactions from the dial-plan.  You can readily write applications that use the AMI and FastAGI protocol together with any of the already-available Twisted protocols.

StarPy is primarily intended to allow Twisted developers to add Asterisk connectivity to their Twisted applications.  It isn't really targeted at the normal AGI-writing populace, as it requires understanding Twisted's asynchronous programming model.  That said, if you do know Twisted, it can readily be used to write stand-alone FastAGIs.

StarPy is Open Source, the we are interested in contributions, bug reports and feedback.  The contributors (listed below) may also be available for implementation and extension contracts.

Installation

StarPy is a pure-Python distutils extension.  Simply unpack the source archive to a temporary directory and run:

python setup.py install

You will need Python 2.3+ and Twisted (Core) installed. You'll need BasicProperty as well.  If you want to check out the GIT version instead of a released version, use:

git clone https://github.com/asterisk-org/starpy.git

On your PythonPath.

The demonstration applications use the utilapplication module, which uses configuration-file-based setup of the AMI and FastAGI servers.  To use this, create a starpy.conf file for the current directory (directory from which to run an example script) or a ~/.starpy.conf user-global file.  Content of the configuration file(s) looks like this:

[AMI]
username=AMIUSERNAME
secret=AMIPASSWORD
server=127.0.0.1
port=5038

[FastAGI]
port=4573
interface=127.0.0.1
context=survey

Keep in mind that FastAGI applications are neither encrypting nor authenticating; you probably should not expose them on any interface other than local (127.0.0.1)!

Asterisk Manager Interface (AMI) Usage

StarPy provides most of the hooks you want to use on the protocol instances.  The AMI client is created by a client factory, as is standard for Twisted operation.  You can create a factory manually like so:

from starpy import manager
f = manager.AMIFactory(sys.argv[1], sys.argv[2])
df = f.login('server',port)

The factory takes the username and secret (password) for the Asterisk manager interface (note: do not actually pass in these values on the command-line in a real application, as this would expose the username and password to anyone on the machine).  The deferred object returned from the login call will fire when the AMI connection has been established and authenticated.  You register callbacks on the deferred to accomplish those tasks you'd like to accomplish.

You will need to configure Asterisk to have the AMI enabled and choose the username, password and allowed hosts in /etc/asterisk/manager.conf.  You will also need to be sure that the AMI user has sufficient permissions to carry out whatever AMI operations you want to perform:

[USERNAME]
secret=SECRETPASSWORD
permit=127.0.0.1
read = system,call,log,verbose,command,agent,user
write = system,call,log,verbose,command,agent,user

Please keep in mind that the AMI interface is not encrypted, so should never be run across an insecure network.  If you need to run across such a network, use ssh tunnelling or the like to prevent eavesdropping!  You will want to read up on the AMI in the voip-info Wiki.

The return value for the login() deferred is an AMIProtocol instance.  The various methods on the AMIProtocol generally handle the creation and interpretation of "Action ID" fields.  The return value for most methods is an event, message or list of events.  Messages and events are modeled as dictionaries with lower-case keys.

Perhaps the most common task desired for use with the AMI Protocol is the creation of new calls.  Here's a snippet showing such generation:

self.ami.originate( 
self.callbackChannel,
self.ourContext, id(self), 1,
timeout = 15,
)

You will likely want to ignore the results of the originate, and instead use an equal timeout waiting for an AGI connection to determine whether you have connected (the AMI originate can "succeed" without a successful connection, and will not tell you what channel is created).  If you want to track whether you have returned from a particular call to originate, use a different extension for each originate call (you can use UtilApplication's waitForCallOn method to register a one-shot handler if you are using UtilApplication).

Another common task is watching for an event of a particular type, for instance a "Hangup" event.  The AMIProtocol instance has a method registerEvent that allows you to add a handler to be called whenever an event of a given type is observed.

def onChannelHangup( ami, event ):
"""Deal with the hangup of an event"""
if event['uniqueid'] == self.uniqueChannelId:
log.info( """AMI Detected close of our channel: %s""", self.uniqueChannelId )
self.stopTime = time.time()
# give the user a few seconds to put down the hand-set
reactor.callLater( 2, df.callback, event )
self.ami.deregisterEvent( 'Hangup', onChannelHangup )
self.ami.registerEvent( 'Hangup', onChannelHangup )
return df.addCallback( self.onHangup, callbacks=5 )

Note that the registerEvent and deregisterEvent methods use object identity to manage the callbacks being stored, as a result, a method is not a good handler (since method objects are created and destroyed each time they are accessed) to choose.  A nested function that can be passed to deregisterHandler is generally a better choice.  Eventually we may use PyDispatcher for the registration as it has solved this problem already in a far more general way.

See the examples/connecttoivr.py and examples/calldurationcallback.py scripts for sample usage of the AMIProtocol

Note that StarPy uses floating-point seconds for all time values in all interfaces,

Fast Asterisk Gateway Interface (FastAGI) Usage

Again, most of the hooks you want to use are provided on the protocol instances.  FastAGI is a server, and is thus created by a (non-client) factory like so:

from starpy import fastagi
f = fastagi.FastAGIFactory(testFunction)
reactor.listenTCP( 4573, f, 50, '127.0.0.1')

testFunction in the example above is the operation to undertake when the Asterisk Server connects to the FastAGI server.  It takes a (connected) FastAGIProtocol instance as its only argument.

This FastAGI protocol has methods available which match those AGI functions documented in the voip-info wiki.  Each method has basic documentation in the automated reference linked above, but you will want to use the wiki documentation to understand the semantics of the calls.  Keep in mind that the execute method (known as exec (which is a Python keyword) in the AGI documentation) allows you to access Asterisk Applications as well as AGI methods.

You use a FastAGI application from your Dial Plan like this (note: arguments do not appear to be passed to FastAGI scripts in Asterisk 1.2.1, unlike regular AGI scripts):

exten => 1000,3,AGI(agi://127.0.0.1:4573)

Please keep in mind that the FastAGI interface is neither encrypted nor authenticating!  It should never be run across an insecure network and should never be run on a port that is accessible from a public network.  Also keep in mind that your FastAGI process must be running already when Asterisk tries to connect to it, you need to code your FastAGI process to be robust so that it is always available to Asterisk.

See the examples directory for examples of FastAGI scripts.

Note that StarPy uses floating-point seconds for all time values in all interfaces,

Sequential Operations

The InSequence class allows for easily setting up multiple chained deferred processes, for instance when you want to play 2 or 3 sound files sequentially.  It is used like this:
sequence = fastagi.InSequence()
sequence.append( agi.setContext, agi.variables['agi_context'] )
sequence.append( agi.setExtension, agi.variables['agi_extension'] )
sequence.append( agi.setPriority, int(agi.variables['agi_priority'])+difference )
sequence.append( agi.finish )
return sequence()

Calling the populated sequence returns a deferred which fires when all elements finish, or any element fails (raise an exception/failure).  The InSequence class is a trivial convenience that avoids needing to define a new callable function for every operation of a many-step operation.

Menu Objects Usage

The FastAGI interface includes basic support for creating hierarchic IVR menus.  The purpose of the menuing system is to encapsulate common UI functionality at a higher level of abstraction than that seen in the raw FastAGI interface.  Menus are defined using "model" classes which describe the desired features of the menu.  An example Menu using simple single-digit Option instances:

m = menu.Menu(
tellInvalid = False, # don't report incorrect selections
prompt = 'atlantic',
options = [
menu.Option( option='0' ),
menu.Option( option='#' ),
menu.ExitOn( option='*' ),
],
maxRepetitions = 5,
)

To invoke the menu, simply call it with a FastAGI protocol instance as its first argument.  The menu will repeat up to maxRepetitions times if an invalid or null entry is chosen.  If tellInvalid is True, the menu will play an "invalid entry" message of your choosing on an unrecognised entry, otherwise it will ignore invalid choices.

If a callable option is specified, such as ExitOn or SubMenu, the result of calling that option with the AGI and the selected option will be returned.  This same mechanism allows for creating chained sub-menus like so:

menu.SubMenu( 
option='1',
menu = menu.Menu(
tellInvalid = False, # don't report incorrect selections
prompt = ['atlantic',menu.DigitsPrompt(53),menu.DateTimePrompt(time.time())],
options = [
menu.Option( option='0' ),
menu.Option( option='#' ),
menu.ExitOn( option='*' ),
],
),
),

which can be used as an option within a higher-level menu.

You can also specify an onSuccess callback in the Option, this will be called (and it's value returned) if and only if that specific Option is chosen by the user (it is called only if the Option is not itself callable (which regular Option instances are not)).

The return value from a Menu is a chain of [ (option, digit), ... ] pairs for the final option selected from the lowest-level menu.  An ExitOn option triggers a return to a higher-level menu; this is not reported as a "final" option selection.

The Menu module includes a CollectDigits class which may be used either as a top-level Menu or as a SubMenu-wrapped option in a higher-level menu:

menu.SubMenu(
option='2',
menu = menu.CollectDigits(
soundFile = 'extension',
maxDigits = 5,
minDigits = 3,
),
)

Eventually the CollectDigits class should support review/cancel options on completion.  It would also be nice to get it to use the prompt system, but as of yet I don't know of any way to make that work with multi-character entry during the various sayXXX functions.

Signalling Errors from FastAGI

The FastAGIProtocol has a method jumpOnError which is intended to be used for implementing the common Asterisk application pattern of setting priority to some large value beyond the current value in order to indicate an error in the application.  Yes, it's an ugly way to signal errors, but there it is.  To use, add jumpOnError to a deferred where you want any uncaught exception to trigger a jump and finish the AGI connection.  This would normally be the overall deferred for your entire FastAGI operation.

df.addErrback( agi.jumpOnError, 100 )

If you only want to cause a particular jump on a particular error/exception or set of exceptions, you can pass in a (tuple of) error classes in the forErrors argument to which to restrict the jump:

df.addErrback( agi.jumpOnError, 50, forErrors=error.OnUnknownUser )

Secondary Services

The utilapplication module contains a few simple classes which provide common services for writing AMI/FastAGI applications.  This includes configuration-file setup of AMI and FastAGI services and an application instance that provides methods for registering to handle incoming FastAGI extensions.

# map incoming calls to extension 's' to the given method onS
APPLICATION.handleCallsFor( 's', someObject.onS )

UtilApplication's agiSpecifier and amiSpecifier property point to automatically generated AGISpecifier and AMISpecifier instances whose parameters are loaded from configuration files.  The specifier instances provide methods for starting up instances configured by the specifier:

# tell the application to run a FastAGI server which dispatches
# to handlers registered with handleCallsFor (as above)
APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall )

# tell the application to log into the configured AMI server
# to allow for further management operations
df = APPLICATION.amiSpecifier.login(
).addCallback( self.onAMIConnect )

Simple FastAGI Application Example

The following is the hellofastagiapp sample application, it uses the starpy.conf file in the current directory to control the FastAGI setup, and shows use of the utilapplication handleCallsFor method, which allows for a single FastAGI server handling many different FastAGI scripts (though in this case we only register a handler for one extension, 's'):

#! /usr/bin/env python
"""FastAGI server using starpy and the utility application framework

This is basically identical to hellofastagi, save that it uses the application
framework to allow for configuration-file-based setup of the AGI service.
"""
from twisted.internet import reactor
from starpy import fastagi, utilapplication
import logging, time

log = logging.getLogger( 'hellofastagi' )

def testFunction( agi ):
"""Demonstrate simplistic use of the AGI interface with sequence of actions"""
log.debug( 'testFunction' )
sequence = fastagi.InSequence()
sequence.append( agi.sayDateTime, time.time() )
sequence.append( agi.finish )
def onFailure( reason ):
log.error( "Failure: %s", reason.getTraceback())
agi.finish()
return sequence().addErrback( onFailure )

if __name__ == "__main__":
logging.basicConfig()
fastagi.log.setLevel( logging.DEBUG )
APPLICATION = utilapplication.UtilApplication()
APPLICATION.handleCallsFor( 's', testFunction )
APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall )
reactor.run()

Changes

StarPy can be downloaded from the project's File Download area.

License

StarPy is licensed under extremely liberal terms.

Copyright (c) 2006, Michael C. Fletcher and Contributors
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.

The name of Michael C. Fletcher, or the name of any Contributor,
may not be used to endorse or promote products derived from this
software without specific prior written permission.

THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY
SITUATION ENDANGERING HUMAN LIFE OR PROPERTY.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

Contributors include (contributors with an (*) after their name are generally available for consulting work):

starpy-1.0.1.0.git.20151124/doc/pydoc/000077500000000000000000000000001274025253300165375ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/doc/pydoc/builddocs.py000077500000000000000000000006661274025253300210740ustar00rootroot00000000000000"""Script to automatically generate PyTable documentation""" import pydoc2 if __name__ == "__main__": excludes = [ "Numeric", "_tkinter", "Tkinter", "math", "string", "twisted", ] stops = [ ] modules = [ 'starpy', 'starpy.examples', '__builtin__', ] pydoc2.PackageDocumentationGenerator( baseModules = modules, destinationDirectory = ".", exclusions = excludes, recursionStops = stops, ).process () starpy-1.0.1.0.git.20151124/doc/pydoc/pydoc2.py000066400000000000000000000356731274025253300203270ustar00rootroot00000000000000"""Pydoc sub-class for generating documentation for entire packages""" import pydoc, inspect, os, string import sys, imp, os, stat, re, types, inspect from repr import Repr from string import expandtabs, find, join, lower, split, strip, rfind, rstrip def classify_class_attrs(cls): """Return list of attribute-descriptor tuples. For each name in dir(cls), the return list contains a 4-tuple with these elements: 0. The name (a string). 1. The kind of attribute this is, one of these strings: 'class method' created via classmethod() 'static method' created via staticmethod() 'property' created via property() 'method' any other flavor of method 'data' not a method 2. The class which defined this attribute (a class). 3. The object as obtained directly from the defining class's __dict__, not via getattr. This is especially important for data attributes: C.data is just a data object, but C.__dict__['data'] may be a data descriptor with additional info, like a __doc__ string. Note: This version is patched to work with Zope Interface-bearing objects """ mro = inspect.getmro(cls) names = dir(cls) result = [] for name in names: # Get the object associated with the name. # Getting an obj from the __dict__ sometimes reveals more than # using getattr. Static and class methods are dramatic examples. if name in cls.__dict__: obj = cls.__dict__[name] else: try: obj = getattr(cls, name) except AttributeError, err: continue # Figure out where it was defined. homecls = getattr(obj, "__objclass__", None) if homecls is None: # search the dicts. for base in mro: if name in base.__dict__: homecls = base break # Get the object again, in order to get it from the defining # __dict__ instead of via getattr (if possible). if homecls is not None and name in homecls.__dict__: obj = homecls.__dict__[name] # Also get the object via getattr. obj_via_getattr = getattr(cls, name) # Classify the object. if isinstance(obj, staticmethod): kind = "static method" elif isinstance(obj, classmethod): kind = "class method" elif isinstance(obj, property): kind = "property" elif (inspect.ismethod(obj_via_getattr) or inspect.ismethoddescriptor(obj_via_getattr)): kind = "method" else: kind = "data" result.append((name, kind, homecls, obj)) return result inspect.classify_class_attrs = classify_class_attrs class DefaultFormatter(pydoc.HTMLDoc): def docmodule(self, object, name=None, mod=None, packageContext = None, *ignored): """Produce HTML documentation for a module object.""" name = object.__name__ # ignore the passed-in name parts = split(name, '.') links = [] for i in range(len(parts)-1): links.append( '%s' % (join(parts[:i+1], '.'), parts[i])) linkedname = join(links + parts[-1:], '.') head = '%s' % linkedname try: path = inspect.getabsfile(object) url = path if sys.platform == 'win32': import nturl2path url = nturl2path.pathname2url(path) filelink = '%s' % (url, path) except TypeError: filelink = '(built-in)' info = [] if hasattr(object, '__version__'): version = str(object.__version__) if version[:11] == '$' + 'Revision: ' and version[-1:] == '$': version = strip(version[11:-1]) info.append('version %s' % self.escape(version)) if hasattr(object, '__date__'): info.append(self.escape(str(object.__date__))) if info: head = head + ' (%s)' % join(info, ', ') result = self.heading( head, '#ffffff', '#7799ee', 'index
' + filelink) modules = inspect.getmembers(object, inspect.ismodule) classes, cdict = [], {} for key, value in inspect.getmembers(object, inspect.isclass): if (inspect.getmodule(value) or object) is object: classes.append((key, value)) cdict[key] = cdict[value] = '#' + key for key, value in classes: for base in value.__bases__: key, modname = base.__name__, base.__module__ module = sys.modules.get(modname) if modname != name and module and hasattr(module, key): if getattr(module, key) is base: if not cdict.has_key(key): cdict[key] = cdict[base] = modname + '.html#' + key funcs, fdict = [], {} for key, value in inspect.getmembers(object, inspect.isroutine): if inspect.isbuiltin(value) or inspect.getmodule(value) is object: funcs.append((key, value)) fdict[key] = '#-' + key if inspect.isfunction(value): fdict[value] = fdict[key] data = [] for key, value in inspect.getmembers(object, pydoc.isdata): if key not in ['__builtins__', '__doc__']: data.append((key, value)) doc = self.markup(pydoc.getdoc(object), self.preformat, fdict, cdict) doc = doc and '%s' % doc result = result + '

%s

\n' % doc packageContext.clean ( classes, object ) packageContext.clean ( funcs, object ) packageContext.clean ( data, object ) if hasattr(object, '__path__'): modpkgs = [] modnames = [] for file in os.listdir(object.__path__[0]): path = os.path.join(object.__path__[0], file) modname = inspect.getmodulename(file) if modname and modname not in modnames: modpkgs.append((modname, name, 0, 0)) modnames.append(modname) elif pydoc.ispackage(path): modpkgs.append((file, name, 1, 0)) modpkgs.sort() contents = self.multicolumn(modpkgs, self.modpkglink) ## result = result + self.bigsection( ## 'Package Contents', '#ffffff', '#aa55cc', contents) result = result + self.moduleSection( object, packageContext) elif modules: contents = self.multicolumn( modules, lambda (key, value), s=self: s.modulelink(value)) result = result + self.bigsection( 'Modules', '#fffff', '#aa55cc', contents) if classes: ## print classes ## import pdb ## pdb.set_trace() classlist = map(lambda (key, value): value, classes) contents = [ self.formattree(inspect.getclasstree(classlist, 1), name)] for key, value in classes: contents.append(self.document(value, key, name, fdict, cdict)) result = result + self.bigsection( 'Classes', '#ffffff', '#ee77aa', join(contents)) if funcs: contents = [] for key, value in funcs: contents.append(self.document(value, key, name, fdict, cdict)) result = result + self.bigsection( 'Functions', '#ffffff', '#eeaa77', join(contents)) if data: contents = [] for key, value in data: try: contents.append(self.document(value, key)) except Exception, err: pass result = result + self.bigsection( 'Data', '#ffffff', '#55aa55', join(contents, '
\n')) if hasattr(object, '__author__'): contents = self.markup(str(object.__author__), self.preformat) result = result + self.bigsection( 'Author', '#ffffff', '#7799ee', contents) if hasattr(object, '__credits__'): contents = self.markup(str(object.__credits__), self.preformat) result = result + self.bigsection( 'Credits', '#ffffff', '#7799ee', contents) return result def classlink(self, object, modname): """Make a link for a class.""" name, module = object.__name__, sys.modules.get(object.__module__) if hasattr(module, name) and getattr(module, name) is object: return '%s' % ( module.__name__, name, name ) return pydoc.classname(object, modname) def moduleSection( self, object, packageContext ): """Create a module-links section for the given object (module)""" modules = inspect.getmembers(object, inspect.ismodule) packageContext.clean ( modules, object ) packageContext.recurseScan( modules ) if hasattr(object, '__path__'): modpkgs = [] modnames = [] for file in os.listdir(object.__path__[0]): path = os.path.join(object.__path__[0], file) modname = inspect.getmodulename(file) if modname and modname not in modnames: modpkgs.append((modname, object.__name__, 0, 0)) modnames.append(modname) elif pydoc.ispackage(path): modpkgs.append((file, object.__name__, 1, 0)) modpkgs.sort() # do more recursion here... for (modname, name, ya,yo) in modpkgs: packageContext.addInteresting( join( (object.__name__, modname), '.')) items = [] for (modname, name, ispackage,isshadowed) in modpkgs: try: # get the actual module object... ## if modname == "events": ## import pdb ## pdb.set_trace() module = pydoc.safeimport( "%s.%s"%(name,modname) ) description, documentation = pydoc.splitdoc( inspect.getdoc( module )) if description: items.append( """%s -- %s"""% ( self.modpkglink( (modname, name, ispackage, isshadowed) ), description, ) ) else: items.append( self.modpkglink( (modname, name, ispackage, isshadowed) ) ) except: items.append( self.modpkglink( (modname, name, ispackage, isshadowed) ) ) contents = string.join( items, '
') result = self.bigsection( 'Package Contents', '#ffffff', '#aa55cc', contents) elif modules: contents = self.multicolumn( modules, lambda (key, value), s=self: s.modulelink(value)) result = self.bigsection( 'Modules', '#fffff', '#aa55cc', contents) else: result = "" return result class AlreadyDone(Exception): pass class PackageDocumentationGenerator: """A package document generator creates documentation for an entire package using pydoc's machinery. baseModules -- modules which will be included and whose included and children modules will be considered fair game for documentation destinationDirectory -- the directory into which the HTML documentation will be written recursion -- whether to add modules which are referenced by and/or children of base modules exclusions -- a list of modules whose contents will not be shown in any other module, commonly such modules as OpenGL.GL, wxPython.wx etc. recursionStops -- a list of modules which will explicitly stop recursion (i.e. they will never be included), even if they are children of base modules. formatter -- allows for passing in a custom formatter see DefaultFormatter for sample implementation. """ def __init__ ( self, baseModules, destinationDirectory = ".", recursion = 1, exclusions = (), recursionStops = (), formatter = None ): self.destinationDirectory = os.path.abspath( destinationDirectory) self.exclusions = {} self.warnings = [] self.baseSpecifiers = {} self.completed = {} self.recursionStops = {} self.recursion = recursion for stop in recursionStops: self.recursionStops[ stop ] = 1 self.pending = [] for exclusion in exclusions: try: self.exclusions[ exclusion ]= pydoc.locate ( exclusion) except pydoc.ErrorDuringImport, value: self.warn( """Unable to import the module %s which was specified as an exclusion module"""% (repr(exclusion))) self.formatter = formatter or DefaultFormatter() for base in baseModules: self.addBase( base ) def warn( self, message ): """Warnings are used for recoverable, but not necessarily ignorable conditions""" self.warnings.append (message) def info (self, message): """Information/status report""" print message def addBase(self, specifier): """Set the base of the documentation set, only children of these modules will be documented""" try: self.baseSpecifiers [specifier] = pydoc.locate ( specifier) self.pending.append (specifier) except pydoc.ErrorDuringImport, value: self.warn( """Unable to import the module %s which was specified as a base module"""% (repr(specifier))) def addInteresting( self, specifier): """Add a module to the list of interesting modules""" if self.checkScope( specifier): ## print "addInteresting", specifier self.pending.append (specifier) else: self.completed[ specifier] = 1 def checkScope (self, specifier): """Check that the specifier is "in scope" for the recursion""" if not self.recursion: return 0 items = string.split (specifier, ".") stopCheck = items [:] while stopCheck: name = string.join(items, ".") if self.recursionStops.get( name): return 0 elif self.completed.get (name): return 0 del stopCheck[-1] while items: if self.baseSpecifiers.get( string.join(items, ".")): return 1 del items[-1] # was not within any given scope return 0 def process( self ): """Having added all of the base and/or interesting modules, proceed to generate the appropriate documentation for each module in the appropriate directory, doing the recursion as we go.""" try: while self.pending: try: if self.completed.has_key( self.pending[0] ): raise AlreadyDone( self.pending[0] ) self.info( """Start %s"""% (repr(self.pending[0]))) object = pydoc.locate ( self.pending[0] ) self.info( """ ... found %s"""% (repr(object.__name__))) except AlreadyDone: pass except pydoc.ErrorDuringImport, value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) except (SystemError, SystemExit), value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) except Exception, value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) else: page = self.formatter.page( pydoc.describe(object), self.formatter.docmodule( object, object.__name__, packageContext = self, ) ) file = open ( os.path.join( self.destinationDirectory, self.pending[0] + ".html", ), 'w', ) file.write(page) file.close() self.completed[ self.pending[0]] = object del self.pending[0] finally: for item in self.warnings: print item def clean (self, objectList, object): """callback from the formatter object asking us to remove those items in the key, value pairs where the object is imported from one of the excluded modules""" for key, value in objectList[:]: for excludeObject in self.exclusions.values(): if hasattr( excludeObject, key ) and excludeObject is not object: if ( getattr( excludeObject, key) is value or (hasattr( excludeObject, '__name__') and excludeObject.__name__ == "Numeric" ) ): objectList[:] = [ (k,o) for k,o in objectList if k != key ] def recurseScan(self, objectList): """Process the list of modules trying to add each to the list of interesting modules""" for key, value in objectList: self.addInteresting( value.__name__ ) if __name__ == "__main__": excludes = [ "OpenGL.GL", "OpenGL.GLU", "OpenGL.GLUT", "OpenGL.GLE", "OpenGL.GLX", "wxPython.wx", "Numeric", "_tkinter", "Tkinter", ] modules = [ "OpenGLContext.debug", ## "wxPython.glcanvas", ## "OpenGL.Tk", ## "OpenGL", ] PackageDocumentationGenerator( baseModules = modules, destinationDirectory = "z:\\temp", exclusions = excludes, ).process () starpy-1.0.1.0.git.20151124/doc/style/000077500000000000000000000000001274025253300165615ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/doc/style/sitestyle.css000066400000000000000000000014211274025253300213160ustar00rootroot00000000000000h1,h2,h3 { color: #000000; background-color: #f0f0f0; border-top-style: solid; border-top-width: 1 } .footer { color: #000033; background-color: #f0f0f0; text-align: center; border-bottom-style: solid; border-bottom-width: 1 } .introduction { margin-left: 60; margin-right: 60; color: #555555; } .technical { margin-left: 60; margin-right: 60; color: #775555; } p { margin-left: 10; margin-right: 10; } ul { margin-left: 30; } pre { background-color: #fffff0; margin-left: 60; } blockquote { margin-left: 90; } body { background-color: #FFFFFF; color: #000000; font-family: Arial, Helvetica; } a:link { color: #3333e0; text-decoration: none; } a:visited { color: #1111aa; text-decoration: none; } a:active { color: #111133; text-decoration: none; } starpy-1.0.1.0.git.20151124/examples/000077500000000000000000000000001274025253300164725ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/examples/__init__.py000066400000000000000000000000751274025253300206050ustar00rootroot00000000000000"""Example applications for usage of StarPy with Asterisk""" starpy-1.0.1.0.git.20151124/examples/amicommand.py000066400000000000000000000020211274025253300211440ustar00rootroot00000000000000#! /usr/bin/env python """Test/sample to call "show database" command """ from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi import utilapplication import menu import os, logging, pprint, time log = logging.getLogger( 'callduration' ) APPLICATION = utilapplication.UtilApplication() def main(): def onConnect( ami ): def onResult( result ): print 'Result', result return ami.logoff() def onError( reason ): print reason.getTraceback() return reason def onFinished( result ): reactor.stop() df = ami.command( 'database show' ) df.addCallbacks( onResult, onError ) df.addCallbacks( onFinished, onFinished ) return df amiDF = APPLICATION.amiSpecifier.login( ).addCallback( onConnect ) if __name__ == "__main__": logging.basicConfig() manager.log.setLevel( logging.DEBUG ) reactor.callWhenRunning( main ) reactor.run() starpy-1.0.1.0.git.20151124/examples/autosurvey/000077500000000000000000000000001274025253300207205ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/examples/autosurvey/extensions.conf000066400000000000000000000004631274025253300237710ustar00rootroot00000000000000; Extensions to allow the autosurvey example application ; to run on the system... include into your extensions.conf ; with a line like: ; #include /home/mcfletch/pylive/starpy/examples/autosurvey/extensions.conf [survey] exten => _X.,1,Answer() exten => _X.,2,AGI(agi://localhost) exten => _X.,3,Hangup() starpy-1.0.1.0.git.20151124/examples/autosurvey/frontend.py000066400000000000000000000123441274025253300231150ustar00rootroot00000000000000"""Simple HTTP Server using twisted.web2""" from nevow import rend, appserver, inevow, tags, loaders from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi, utilapplication from basicproperty import common, basic, propertied, weak import os, logging, pprint, time log = logging.getLogger( 'autosurvey' ) class Application( utilapplication.UtilApplication ): """Services provided at the application level""" surveys = common.DictionaryProperty( "surveys", """Set of surveys indexed by survey/extension number""", ) class Survey( propertied.Propertied ): """Models a single survey to be completed""" surveyId = common.IntegerProperty( "surveyId", """Unique identifier for this survey""", ) owner = basic.BasicProperty( "owner", """Owner's phone number to which to connect""", ) questions = common.ListProperty( "questions", """Set of questions which make up the survey""", ) YOU_CURRENTLY_HAVE = 'vm-youhave' QUESTIONS_IN_YOUR_SURVEY = 'vm-messages' QUESTION_IN_YOUR_SURVEY = 'vm-message' TO_LISTEN_TO_SURVEY_QUESTION = 'to-listen-to-it' TO_RECORD_A_NEW_SURVEY_QUESTION = 'to-rerecord-it' TO_FINISH_SURVEY_SETUP = 'vm-helpexit' def setupSurvey( self, agi ): """AGI application to allow the user to set up the survey Screen 1: You have # questions. To listen to a question, press the number of the question. To record a new question, press pound. To finish setup, press star. """ seq = fastagi.InSequence( ) seq.append( agi.wait, 2 ) base = """You currently have %s question%s. To listen to a question press the number of the question. To record a new question, press pound. To finish survey setup, press star. """%( len(self.questions), ['','s'][len(self.questions)==1], ) if len(base) != 1: base += 's' base = " ".join(base.split()) seq.append( agi.execute, 'Festival', base ) seq.append( agi.finish, ) return seq() seq.append( agi.streamFile, self.YOU_CURRENTLY_HAVE ) seq.append( agi.sayNumber, len(self.questions)) if len(self.questions) == 1: seq.append( agi.streamFile, self.QUESTION_IN_YOUR_SURVEY ) else: seq.append( agi.streamFile, self.QUESTIONS_IN_YOUR_SURVEY ) seq.append( agi.streamFile, self.TO_LISTEN_TO_SURVEY_QUESTION ) seq.append( agi.streamFile, self.TO_RECORD_A_NEW_SURVEY_QUESTION ) seq.append( agi.streamFile, self.TO_FINISH_SURVEY_SETUP ) seq.append( agi.finish, ) return seq() def newQuestionId( self ): """Return a new, unique, question id""" import random, sys bad = True while bad: bad = False id = random.randint(0,sys.maxint) for question in self.questions: if id == question.__dict__.get('questionId'): bad = True return id class Question( propertied.Propertied ): survey = weak.WeakProperty( "survey", """Our survey object""", ) questionId = common.IntegerProperty( "questionId", """Unique identifier for our question""", defaultFunction = lambda prop,client: client.survey.newQuestionId(), ) def recordQuestion( self, agi, number=None ): """Record a question (number)""" return agi.recordFile( '%s.%s'%(self.survey.surveyId,self.questionId), 'gsm', '#*', timeout=60, beep = True, silence=5, ).addCallback( self.onRecorded, agi=agi ).addErrback(self.onRecordAborted, agi=agi ) def onRecorded( self, result, agi ): """Handle recording of the question""" def getManagerAPI( username, password, server='127.0.0.1', port=5038 ): """Retrieve a logged-in manager API""" class SurveySetup(rend.Page): """Page displaying the survey setup""" addSlash = True docFactory = loaders.htmlfile( 'index.html' ) class RecordFunction( rend.Page ): """Page/application to record survey via call to user""" def renderHTTP( self, ctx ): """Process rendering of the request""" # process request parameters... request = inevow.IRequest( ctx ) # XXX sanitise and check value... channel = 'SIP/%s'%( request.args['ownerName'][0], ) df = APPLICATION.amiSpecifier.login() def onLogin( ami ): # Note that the connect comes in *before* the originate returns, # so we need to wait for the call before we even send it... userConnectDF = APPLICATION.waitForCallOn( '23', timeout=15 ) APPLICATION.surveys['23'] = survey = Survey() userConnectDF.addCallback( survey.setupSurvey, ) def onComplete( result ): return ami.logoff() ami.originate(# don't wait for this to complete... # XXX handle case where the originate fails differently # from the case where we just don't get a connection? channel, APPLICATION.agiSpecifier.context, '23', '1', timeout=14, ).addCallbacks( onComplete, onComplete ) return userConnectDF return df.addCallback( onLogin ) def main(): """Create the web-site""" s = SurveySetup() s.putChild( 'record', RecordFunction() ) site = appserver.NevowSite(s) webServer = internet.TCPServer(8080, site) webServer.startService() if __name__ == "__main__": logging.basicConfig() log.setLevel( logging.DEBUG ) manager.log.setLevel( logging.DEBUG ) fastagi.log.setLevel( logging.DEBUG ) APPLICATION = Application() APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) from twisted.internet import reactor reactor.callWhenRunning( main ) reactor.run() starpy-1.0.1.0.git.20151124/examples/autosurvey/index.html000066400000000000000000000025051274025253300227170ustar00rootroot00000000000000 Autosurvey Demo Application

Autosurvey Demo Application

This demonstration shows how to use StarPy to construct a simple automated phone-survey application for use in polling group members regarding decisions which need to be made.

Features:

Setup

Enter your phone number here:

The survey server will call you to record the survey options...

Introduction

Options

Enter the participant's phone numbers here:

Results

starpy-1.0.1.0.git.20151124/examples/calldurationcallback.py000066400000000000000000000166301274025253300232100ustar00rootroot00000000000000#! /usr/bin/env python """Sample application to read call duration back to user Implemented as an AGI and a manager connection, send those who want to time the call to the AGI, we will wait for the end of the call, then call them back with the duration message. """ from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi import utilapplication import menu import os, logging, pprint, time log = logging.getLogger( 'callduration' ) class Application( utilapplication.UtilApplication ): """Application for the call duration callback mechanism""" def onS( self, agi ): """Incoming AGI connection to the "s" extension (start operation)""" log.info( """New call tracker""" ) c = CallTracker() return c.recordChannelInfo( agi ).addErrback( agi.jumpOnError, difference=100, ) class CallTracker( object ): """Object which tracks duration of a single call This object encapsulates the entire interaction with the user, from the initial incoming FastAGI that records the channel ID and account number through the manager watching for the disconnect to the new call setup and the FastAGI that plays back the results... Requires a context 'callduration' with 's' mapping to this AGI, as well as all numeric extensions. """ ourContext = 'callduration' def __init__( self ): """Initialise the tracker object""" self.uniqueChannelId = None self.currentChannel = None self.callbackChannel = None self.account = None self.cancelled = False self.ami = None self.startTime = None self.stopTime = None def recordChannelInfo( self, agi ): """Records relevant channel information, creates manager watcher""" self.uniqueChannelId = agi.variables['agi_uniqueid'] self.currentChannel = currentChannel = agi.variables['agi_channel'] # XXX everything up to the last - is normally our local caller's "address" # this is not, however, a great way to decide who to call back... self.callbackChannel = currentChannel.rsplit( '-', 1)[0] # Ask user for the account number... df = menu.CollectDigits( soundFile = 'your-account', maxDigits = 7, minDigits = 3, timeout = 5, )( agi ).addCallback( self.onAccountInput,agi=agi, ) # XXX handle AMI login failure... amiDF = APPLICATION.amiSpecifier.login( ).addCallback( self.onAMIConnect ) dl = defer.DeferredList( [df, amiDF] ) return dl.addCallback( self.onConnectAndAccount ) def onAccountInput( self, result, agi, retries=2): """Allow user to enter again if timed out""" self.account = result[0][1] self.startTime = time.time() agi.finish() # let the user go about their business... return agi def cleanUp( self, agi=None ): """Cleanup on error as much as possible""" items = [] if self.ami: items.append( self.ami.logoff()) self.ami = None if items: return defer.DeferredList( items ) else: return defer.succeed( False ) def onAMIConnect( self, ami ): """We have successfully connected to the AMI""" log.debug( "AMI login complete" ) if not self.cancelled: self.ami = ami return ami else: return self.ami.logoff() def onConnectAndAccount( self, results ): """We have connected and retrieved an account""" log.info( """AMI Connected and account information gathered: %s""", self.uniqueChannelId ) df = defer.Deferred() def onChannelHangup( ami, event ): """Deal with the hangup of an event""" if event['uniqueid'] == self.uniqueChannelId: log.info( """AMI Detected close of our channel: %s""", self.uniqueChannelId ) self.stopTime = time.time() # give the user a few seconds to put down the hand-set reactor.callLater( 2, df.callback, event ) self.ami.deregisterEvent( 'Hangup', onChannelHangup ) log.debug( 'event:', event ) if not self.cancelled: self.ami.registerEvent( 'Hangup', onChannelHangup ) return df.addCallback( self.onHangup, callbacks=5 ) def onHangup( self, event, callbacks=5 ): """Okay, the call is finished, time to inform the user""" log.debug( 'onHangup %s %s', event, callbacks ) def ignoreResult( result ): """Since we're using an equal timeout waiting for a connect we don't care *how* this fails/succeeds""" pass self.ami.originate( self.callbackChannel, self.ourContext, id(self), 1, timeout = 15, ).addCallbacks( ignoreResult, ignoreResult ) df = APPLICATION.waitForCallOn( id(self), 15 ) df.addCallbacks( self.onUserReconnected, self.onUserReconnectFail, errbackKeywords = { 'event': event, 'callbacks': callbacks-1 }, ) def onUserReconnectFail( self, reason, event, callbacks ): """Wait for bit, then retry...""" if callbacks: # XXX really want something like a decaying back-off in frequency # with final values of e.g. an hour... log.info( """Failure connecting: will retry in 30 seconds""" ) reactor.callLater( 30, self.onHangup, event, callbacks ) else: log.error( """Unable to connect to user, giving up""" ) return self.cleanUp( None ) def onUserReconnected( self, agi ): """Handle the user interaction after they've re-connected""" log.info( """Connection re-established with the user""" ) # XXX should handle unexpected failures in here... delta = self.stopTime - self.startTime minutes, seconds = divmod( delta, 60 ) seconds = int(seconds) hours, minutes = divmod( minutes, 60 ) duration = [] if hours: duration.append( '%s hour%s'%(hours,['','s'][hours!=1])) if minutes: duration.append( '%s second%s'%(minutes,['','s'][minutes!=1])) if seconds: duration.append( '%s second%s'%(seconds,['','s'][seconds!=1])) if not duration: duration = '0' else: duration = " ".join( duration ) seq = fastagi.InSequence( ) seq.append( agi.wait, 1 ) seq.append( agi.execute, "Festival", "Call to account %r took %s"%(self.account,duration) ) seq.append( agi.wait, 1 ) seq.append( agi.execute, "Festival", "Repeating, call to account %r took %s"%(self.account,duration) ) seq.append( agi.wait, 1 ) seq.append( agi.finish ) def logSuccess( ): log.debug( """Finished successfully!""" ) return defer.succeed( True ) seq.append( logSuccess ) seq.append( self.cleanUp, agi ) return seq() APPLICATION = Application() if __name__ == "__main__": logging.basicConfig() log.setLevel( logging.DEBUG ) #manager.log.setLevel( logging.DEBUG ) #fastagi.log.setLevel( logging.DEBUG ) APPLICATION.handleCallsFor( 's', APPLICATION.onS ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) from twisted.internet import reactor reactor.run() starpy-1.0.1.0.git.20151124/examples/calldurationextensions.conf000066400000000000000000000011251274025253300241410ustar00rootroot00000000000000; Extensions to allow the autosurvey example application ; to run on the system... include into your extensions.conf ; with a line like: ; #include /home/mcfletch/pylive/starpy/examples/calldurationextensions.conf ; You need to Goto(callduration,s,1) for those calls for which you want to have ; callduration support for [regulardial] exten => s,1,Dial(SIP/3333@testout) exten => s,2,Hangup() [callduration] exten => s,1,Answer() exten => s,2,AGI(agi://localhost:4576) exten => s,3,Goto(regulardial,s,1) exten => _X.,1,Answer() exten => _X.,2,AGI(agi://localhost:4576) exten => _X.,3,Hangup() starpy-1.0.1.0.git.20151124/examples/connecttoivr.py000066400000000000000000000021061274025253300215600ustar00rootroot00000000000000"""Example script to generate a call to connect a remote channel to an IVR""" from starpy import manager from twisted.internet import reactor import sys, logging def main( channel = 'sip/20035@aci.on.ca', connectTo=('outgoing','s','1') ): f = manager.AMIFactory(sys.argv[1], sys.argv[2]) df = f.login() def onLogin( protocol ): """On Login, attempt to originate the call""" context, extension, priority = connectTo df = protocol.originate( channel, context,extension,priority, ) def onFinished( result ): df = protocol.logoff() def onLogoff( result ): reactor.stop() return df.addCallbacks( onLogoff, onLogoff ) def onFailure( reason ): print reason.getTraceback() return reason df.addErrback( onFailure ) df.addCallbacks( onFinished, onFinished ) return df def onFailure( reason ): """Unable to log in!""" print reason.getTraceback() reactor.stop() df.addCallbacks( onLogin, onFailure ) return df if __name__ == "__main__": manager.log.setLevel( logging.DEBUG ) logging.basicConfig() reactor.callWhenRunning( main ) reactor.run() starpy-1.0.1.0.git.20151124/examples/connecttoivrapp.py000066400000000000000000000021021274025253300222550ustar00rootroot00000000000000"""Example script to generate a call to connect a remote channel to an IVR This version of the script uses the utilapplication framework and is pared down for presentation on a series of slides """ from starpy import manager import utilapplication from twisted.internet import reactor import sys, logging APPLICATION = utilapplication.UtilApplication() def main( channel = 'sip/4167290048@testout', connectTo=('outgoing','s','1') ): df = APPLICATION.amiSpecifier.login() def onLogin( protocol ): """We've logged into the manager, generate a call and log off""" context, extension, priority = connectTo df = protocol.originate( channel, context,extension,priority, ) def onFinished( result ): return protocol.logoff() df.addCallbacks( onFinished, onFinished ) return df def onFailure( reason ): print reason.getTraceback() def onFinished( result ): reactor.stop() df.addCallbacks( onLogin, onFailure ).addCallbacks( onFinished, onFinished ) return df if __name__ == "__main__": logging.basicConfig() reactor.callWhenRunning( main ) reactor.run() starpy-1.0.1.0.git.20151124/examples/fastagisetvariable.py000066400000000000000000000015521274025253300227070ustar00rootroot00000000000000#! /usr/bin/env python """Try to set a FastAGI variable""" from twisted.internet import reactor from starpy import fastagi import utilapplication import logging, time log = logging.getLogger( 'hellofastagi' ) def testFunction( agi ): """Demonstrate simplistic use of the AGI interface with sequence of actions""" log.debug( 'testFunction' ) def setX( ): return agi.setVariable( 'this"toset', 'That"2set' ) def getX( result ): return agi.getVariable( 'this"toset' ) def onX( value ): print 'Retrieved value', value reactor.stop() return setX().addCallback( getX ).addCallbacks( onX, onX ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.DEBUG ) APPLICATION = utilapplication.UtilApplication() APPLICATION.handleCallsFor( 's', testFunction ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) reactor.run() starpy-1.0.1.0.git.20151124/examples/getvariable.py000066400000000000000000000031041274025253300213270ustar00rootroot00000000000000#! /usr/bin/env python """Demonstrate usage of getVariable on the agi interface... """ from twisted.internet import reactor from starpy import fastagi import utilapplication import logging, time, pprint log = logging.getLogger( 'hellofastagi' ) def envVars( agi ): """Print out channel variables for display""" vars = [ x.split( ' -- ' )[0].strip() for x in agi.getVariable.__doc__.splitlines() if len(x.split( ' -- ' )) == 2 ] for var in vars: yield var def printVar( result, agi, vars ): """Print out the variables produced by envVars""" def doPrint( result, var ): print '%r -- %r'%( var, result ) def notAvailable( reason, var ): print '%r -- UNDEFINED'%( var, ) try: var = vars.next() except StopIteration, err: return None else: return agi.getVariable( var ).addCallback( doPrint, var ).addErrback( notAvailable, var, ).addCallback( printVar, agi, vars, ) def testFunction( agi ): """Print out known AGI variables""" log.debug( 'testFunction' ) print 'AGI Variables' pprint.pprint( agi.variables ) print 'Channel Variables' sequence = fastagi.InSequence() sequence.append( printVar, None, agi, envVars(agi) ) sequence.append( agi.finish ) def onFailure( reason ): log.error( "Failure: %s", reason.getTraceback()) agi.finish() return sequence().addErrback( onFailure ) if __name__ == "__main__": logging.basicConfig() #fastagi.log.setLevel( logging.DEBUG ) APPLICATION = utilapplication.UtilApplication() APPLICATION.handleCallsFor( 's', testFunction ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) reactor.run() starpy-1.0.1.0.git.20151124/examples/hellofastagi.py000066400000000000000000000014411274025253300215060ustar00rootroot00000000000000#! /usr/bin/env python """Simple FastAGI server using starpy""" from twisted.internet import reactor from starpy import fastagi import logging, time log = logging.getLogger( 'hellofastagi' ) def testFunction( agi ): """Demonstrate simplistic use of the AGI interface with sequence of actions""" log.debug( 'testFunction' ) sequence = fastagi.InSequence() sequence.append( agi.sayDateTime, time.time() ) sequence.append( agi.finish ) def onFailure( reason ): log.error( "Failure: %s", reason.getTraceback()) agi.finish() return sequence().addErrback( onFailure ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.DEBUG ) f = fastagi.FastAGIFactory(testFunction) reactor.listenTCP(4573, f, 50, '127.0.0.1') # only binding on local interface reactor.run() starpy-1.0.1.0.git.20151124/examples/hellofastagiapp.py000066400000000000000000000020351274025253300222070ustar00rootroot00000000000000#! /usr/bin/env python """FastAGI server using starpy and the utility application framework This is basically identical to hellofastagi, save that it uses the application framework to allow for configuration-file-based setup of the AGI service. """ from twisted.internet import reactor from starpy import fastagi import utilapplication import logging, time log = logging.getLogger( 'hellofastagi' ) def testFunction( agi ): """Demonstrate simplistic use of the AGI interface with sequence of actions""" log.debug( 'testFunction' ) sequence = fastagi.InSequence() sequence.append( agi.sayDateTime, time.time() ) sequence.append( agi.finish ) def onFailure( reason ): log.error( "Failure: %s", reason.getTraceback()) agi.finish() return sequence().addErrback( onFailure ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.DEBUG ) APPLICATION = utilapplication.UtilApplication() APPLICATION.handleCallsFor( 's', testFunction ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) reactor.run() starpy-1.0.1.0.git.20151124/examples/menu.py000066400000000000000000000633161274025253300200210ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """IVR-based menuing system with retry, exit, and similar useful features You use the menuing system by instantiating Interaction and Option sub-classes as a tree of options that make up an IVR menu. Calling the top-level menu produces a Deferred that fires with a list of [(Option,value),...] pairs, where Option is the thing chosen and value is the value entered by the user for choosing that option. When programming an IVR you will likely want to make Option sub-classes that are callable to accomplish the task indicated by the user. XXX allow for starting the menu system anywhere in the hierarchy XXX add the reject/accept menus to the CollectDigits (requires soundfiles in standard locations on the server, complicates install) """ from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi, error import utilapplication import os, logging, pprint, time from basicproperty import common, propertied, basic log = logging.getLogger('menu') log.setLevel(logging.DEBUG) class Interaction(propertied.Propertied): """Base class for user-interaction operations""" ALL_DIGITS = '0123456789*#' timeout = common.FloatProperty( "timeout", """Duration to wait for response before repeating message""", defaultValue = 5, ) maxRepetitions = common.IntegerProperty( "maxRepetitions", """Maximum number of times to play before failure""", defaultValue = 5, ) onSuccess = basic.BasicProperty( "onSuccess", """Optional callback for success with signature method( result, runner )""", ) onFailure = basic.BasicProperty( "onFailure", """Optional callback for failure with signature method( result, runner )""", ) runnerClass = None def __call__(self, agi, *args, **named): """Initiate AGI-based interaction with the user""" return self.runnerClass(model=self, agi=agi)(*args, **named) class Runner(propertied.Propertied): """User's interaction with a given Interaction-type""" agi = basic.BasicProperty( "agi", """The AGI instance we use to communicate with the user""", ) def defaultFinalDF(prop, client): """Produce the default finalDF with onSuccess/onFailure support""" df = defer.Deferred() model = client.model if hasattr(model, 'onSuccess'): log.debug('register onSuccess: %s', model.onSuccess) df.addCallback(model.onSuccess, runner=client) if hasattr(model, 'onFailure'): log.debug('register onFailure: %s', model.onFailure) df.addErrback(model.onFailure, runner=client) return df finalDF = basic.BasicProperty( "finalDF", """Final deferred we will callback/errback on success/failure""", defaultFunction = defaultFinalDF, ) del defaultFinalDF alreadyRepeated = common.IntegerProperty( "alreadyRepeated", """Number of times we've repeated the message...""", defaultValue = 0, ) model = basic.BasicProperty( "model", """The data-model that we are presenting to the user (e.g. Menu)""", ) def returnResult(self, result): """Return result of deferred to our original caller""" log.debug('returnResult: %s %s', self.model,result) if not self.finalDF.called: self.finalDF.debug = True self.finalDF.callback(result) else: log.debug('finalDF already called, ignoring %s', result) return result def returnError(self, reason): """Return failure of deferred to our original caller""" log.debug('returnError: %s', self.model) if not isinstance(reason.value, error.MenuExit): log.warn("""Failure during menu: %s""", reason.getTraceback()) if not self.finalDF.called: self.finalDF.debug = True self.finalDF.errback(reason) else: log.debug('finalDF already called, ignoring %s', reason.getTraceback()) def promptAsRunner(self, prompt): """Take set of prompt-compatible objects and produce a PromptRunner for them""" realPrompt = [] for p in prompt: if isinstance(p, (str, unicode)): p = AudioPrompt(p) elif isinstance(p, int): p = NumberPrompt(p) elif not isinstance(p, Prompt): raise TypeError( """Unknown prompt element type on %r: %s"""%( p, p.__class__, )) realPrompt.append(p) return PromptRunner( elements = realPrompt, escapeDigits = self.escapeDigits, agi = self.agi, timeout = self.model.timeout, ) class CollectDigitsRunner(Runner): """User's single interaction to enter a set of digits Note: Asterisk is hard-coded to use # to exit the entry-mode... """ def __call__(self, *args, **named): """Begin the AGI processing for the menu""" self.readDigits() return self.finalDF def readDigits(self, result=None): """Begin process of reading digits from the user""" soundFile = getattr(self.model, 'soundFile', None) if soundFile: # easiest possibility, just read out the file... return self.agi.getData( soundFile, timeout=self.model.timeout, maxDigits = getattr(self.model, 'maxDigits', None), ).addCallback(self.onReadDigits).addErrback(self.returnError) else: raise NotImplemented("""Haven't got non-soundfile menus working yet""") self.agi.getData(self.menu. filename, timeout=2.000, maxDigits=None) def validEntry(self, digits): """Determine whether given digits are considered a "valid" entry""" minDigits = getattr(self.model, 'minDigits', None) if minDigits is not None: if len(digits) < minDigits: return False, 'Too few digits' return True, None def onReadDigits(self, (digits,timeout)): """Deal with succesful result from reading digits""" log.info("""onReadDigits: %r, %s""", digits, timeout) valid, reason = self.validEntry(digits) if (not digits) and (not timeout): # user pressed # raise error.MenuExit( self.model, """User cancelled entry of digits""", ) if not valid: if self.model.tellInvalid: # this should be a menu, letting the user decide to re-enter, # or cancel entry pass self.alreadyRepeated += 1 if self.alreadyRepeated >= self.model.maxRepetitions: log.warn("""User did not complete digit-entry for %s, timing out""", self.model) raise error.MenuTimeout( self.model, """User did not finish digit-entry in %s passes of collection""" % ( self.alreadyRepeated, ) ) return self.readDigits() else: # Yay, we got a valid response! return self.returnResult([(self, digits)]) 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 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 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 Menu(Interaction): """IVR-based menu, returns options selected by the user and keypresses The Menu holds a collection of Option instances along with a prompt which presents those options to the user. The menu will attempt to collect the user's selected option up to maxRepetitions times, playing the prompt each time. If tellInvalid is true, will allow any character being pressed to stop the playback, and will tell the user if the pressed character is not recognised. Otherwise will simply ignore a pressed character which isn't part of an Option object's 'option' property. The menu will chain into callable Options, so that SubMenu and ExitOn can be used to produce effects such as multi-level menus with options to return to the parent menu level. Returns [(option,char(pressedKey))...] for each level of menu explored """ INVALID_OPTION_FILE = 'pm-invalid-option' 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""", ) options = common.ListProperty( "options", """Set of options the user may select""", ) tellInvalid = common.IntegerProperty( "tellInvalid", """Whether to tell the user that their selection is unrecognised""", defaultValue = True, ) runnerClass = MenuRunner 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 SubMenu(Option): """A menu-holding option, just forwards call to the held menu""" menu = basic.BasicProperty( "menu", """The sub-menu we are presenting to the user""", ) def __call__(self, pressed, parent): """Get result from the sub-menu, add ourselves into the result""" def onResult(result): log.debug("""Child menu %s result: %s""", self.menu, result) result.insert(0, (self,pressed)) return result def onFailure(reason): """Trap voluntary exit and re-start the parent menu""" reason.trap(error.MenuExit) log.warn("""Restarting parent menu: %s""", parent) return parent.model(parent.agi) return self.menu(parent.agi).addCallbacks(onResult, onFailure) class ExitOn(Option): """An option which exits from the current menu level""" def __call__(self, pressed, parent): """Raise a MenuExit error""" raise error.MenuExit( self, pressed, parent, """User selected ExitOn option""", ) 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 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 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 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: 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 class Prompt(propertied.Propertied): """A Prompt to be read to the user""" value = basic.BasicProperty( "value", """Filename to be read to the user""", ) def __init__(self, value, **named): named['value'] = value super(Prompt, self).__init__(**named) class AudioPrompt(Prompt): """Default type of prompt, reads a file""" def read(self, agi, escapeDigits): """Read the audio prompt to the user""" # There's no "say file" operation... return agi.getOption(self.value, escapeDigits, 0.001) class TextPrompt(Prompt): """Prompt produced via festival text-to-speech reader (built-in command)""" def read(self, agi, escapeDigits): return agi.execute("Festival", self.value, escapeDigits) class NumberPrompt(Prompt): """Prompt that reads a number as a number""" value = common.IntegerProperty( "value", """Integer numeral to read""", ) def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayNumber(self.value, escapeDigits) class DigitsPrompt(Prompt): """Prompt that reads a number as digits""" def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayDigits(self.value, escapeDigits) class AlphaPrompt(Prompt): """Prompt that reads alphabetic string as characters""" def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayAlpha(self.value, escapeDigits) class DateTimePrompt(Prompt): """Prompt that reads a date/time as a date""" format = basic.BasicProperty( "format", """Format in which to read the date to the user""", defaultValue = None ) def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayDateTime(self.value, escapeDigits, format=self.format) starpy-1.0.1.0.git.20151124/examples/menutest.py000066400000000000000000000050151274025253300207110ustar00rootroot00000000000000#! /usr/bin/env python """Sample application to test the menuing utility classes""" from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi, error import utilapplication import menu import os, logging, pprint, time log = logging.getLogger( 'menutest' ) mainMenu = menu.Menu( prompt = '/home/mcfletch/starpydemo/soundfiles/menutest-toplevel', #prompt = 'houston', textPrompt = '''Top level of the menu test example Pressing Star will exit this menu at any time. Options zero and pound will exit with those options selected. Option one will start a submenu. Option two will start a digit-collecting sub-menu. We'll tell you if you make an invalid selection here.''', options = [ menu.Option( option='0' ), menu.Option( option='#' ), menu.ExitOn( option='*' ), menu.SubMenu( option='1', menu = menu.Menu( prompt = '/home/mcfletch/starpydemo/soundfiles/menutest-secondlevel', #prompt = 'atlantic', textPrompt = '''A second-level menu in the menu test example Pressing Star will exit this menu at any time. Options zero and pound will exit the whole menu with those options selected. We won't tell you if you make an invalid selection here. ''', tellInvalid = False, # don't report incorrect selections options = [ menu.Option( option='0' ), menu.Option( option='#' ), menu.ExitOn( option='*' ), ], ), ), menu.SubMenu( option='2', menu = menu.CollectDigits( textPrompt = '''Digit collection example, Please enter three to 5 digits. ''', soundFile = '/home/mcfletch/starpydemo/soundfiles/menutest-digits', #soundFile = 'extension', maxDigits = 5, minDigits = 3, ), ), ], ) class Application( utilapplication.UtilApplication ): """Application for the call duration callback mechanism""" def onS( self, agi ): """Incoming AGI connection to the "s" extension (start operation)""" log.info( """New call tracker""" ) def onComplete( result ): log.info( """Final result: %r""", result ) agi.finish() return mainMenu( agi ).addCallbacks( onComplete, onComplete ) APPLICATION = Application() if __name__ == "__main__": logging.basicConfig() log.setLevel( logging.DEBUG ) #manager.log.setLevel( logging.DEBUG ) fastagi.log.setLevel( logging.DEBUG ) menu.log.setLevel( logging.DEBUG ) APPLICATION.handleCallsFor( 's', APPLICATION.onS ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) from twisted.internet import reactor reactor.run() starpy-1.0.1.0.git.20151124/examples/menutestextensions.conf000066400000000000000000000005041274025253300233240ustar00rootroot00000000000000; Extensions to allow the menutest example application ; to run on the system... include into your extensions.conf ; with a line like: ; #include /home/mcfletch/pylive/starpy/examples/menutestextensions.conf [menutest] exten => s,1,Answer() exten => s,2,Wait(1) exten => s,3,AGI(agi://localhost:4575) exten => s,4,Hangup() starpy-1.0.1.0.git.20151124/examples/priexhaustion.py000066400000000000000000000077441274025253300217620ustar00rootroot00000000000000#! /usr/bin/env python """Sample application to watch for PRI exhaustion This script watches for events on the AMI interface, tracking the identity of open channels in order to track how many channels are being used. This would be used to send messages to an administrator when network capacity is being approached. Similarly, you could watch for spare capacity on the network and use that to decide whether to allow low-priority calls, such as peering framework or free-world-dialup calls to go through. """ from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi import utilapplication import menu import os, logging, pprint, time from basicproperty import common, propertied, basic log = logging.getLogger( 'priexhaustion' ) log.setLevel( logging.INFO ) class ChannelTracker( propertied.Propertied ): """Track open channels on the Asterisk server""" channels = common.DictionaryProperty( "channels", """Set of open channels on the system""", ) thresholdCount = common.IntegerProperty( "thresholdCount", """Storage of threshold below which we don't warn user""", defaultValue = 20, ) def main( self ): """Main operation for the channel-tracking demo""" amiDF = APPLICATION.amiSpecifier.login( ).addCallback( self.onAMIConnect ) # XXX do something useful on failure to login... def onAMIConnect( self, ami ): """Register for AMI events""" # XXX should do an initial query to populate channels... # XXX should handle asterisk reboots (at the moment the AMI # interface will just stop generating events), not a practical # problem at the moment, but should have a periodic check to be sure # the interface is still up, and if not, should close and restart log.debug( 'onAMIConnect' ) ami.status().addCallback( self.onStatus, ami=ami ) ami.registerEvent( 'Hangup', self.onChannelHangup ) ami.registerEvent( 'Newchannel', self.onChannelNew ) def interestingEvent( self, event, ami=None ): """Decide whether this channel event is interesting Real-world application would want to take only Zap channels, or only channels from a given context, or whatever other filter you want in order to capture *just* the scarce resource (such as PRI lines). Keep in mind that an "interesting" event must show up as interesting for *both* Newchannel and Hangup events or you will leak references/channels or have unknown channels hanging up. """ return True def onStatus( self, events, ami=None ): """Integrate the current status into our set of channels""" log.debug( """Initial channel status retrieved""" ) for event in events: self.onChannelNew( ami, event ) def onChannelNew( self, ami, event ): """Handle creation of a new channel""" log.debug( """Start on channel %s""", event ) if self.interestingEvent( event, ami ): opening = not self.channels.has_key( event['uniqueid'] ) self.channels[ event['uniqueid'] ] = event if opening: self.onChannelChange( ami, event, opening = opening ) def onChannelHangup( self, ami, event ): """Handle hangup of an existing channel""" if self.interestingEvent( event, ami ): try: del self.channels[ event['uniqueid']] except KeyError, err: log.warn( """Hangup on unknown channel %s""", event ) else: log.debug( """Hangup on channel %s""", event ) self.onChannelChange( ami, event, opening = False ) def onChannelChange( self, ami, event, opening=False ): """Channel count has changed, do something useful like enforcing limits""" if opening and len(self.channels) > self.thresholdCount: log.warn( """Current channel count: %s""", len(self.channels ) ) else: log.info( """Current channel count: %s""", len(self.channels ) ) APPLICATION = utilapplication.UtilApplication() if __name__ == "__main__": logging.basicConfig() #log.setLevel( logging.DEBUG ) #manager.log.setLevel( logging.DEBUG ) #fastagi.log.setLevel( logging.DEBUG ) tracker = ChannelTracker() reactor.callWhenRunning( tracker.main ) reactor.run() starpy-1.0.1.0.git.20151124/examples/priexhaustionbare.py000066400000000000000000000045411274025253300226040ustar00rootroot00000000000000#! /usr/bin/env python from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi import utilapplication import menu import os, logging, pprint, time from basicproperty import common, propertied, basic log = logging.getLogger( 'priexhaustion' ) log.setLevel( logging.INFO ) class ChannelTracker( propertied.Propertied ): """Track open channels on the Asterisk server""" channels = common.DictionaryProperty( "channels", """Set of open channels on the system""", ) thresholdCount = common.IntegerProperty( "thresholdCount", """Storage of threshold below which we don't warn user""", defaultValue = 20, ) def main( self ): """Main operation for the channel-tracking demo""" amiDF = APPLICATION.amiSpecifier.login( ).addCallback( self.onAMIConnect ) def onAMIConnect( self, ami ): ami.status().addCallback( self.onStatus, ami=ami ) ami.registerEvent( 'Hangup', self.onChannelHangup ) ami.registerEvent( 'Newchannel', self.onChannelNew ) def onStatus( self, events, ami=None ): """Integrate the current status into our set of channels""" log.debug( """Initial channel status retrieved""" ) for event in events: self.onChannelNew( ami, event ) def onChannelNew( self, ami, event ): """Handle creation of a new channel""" log.debug( """Start on channel %s""", event ) opening = not self.channels.has_key( event['uniqueid'] ) self.channels[ event['uniqueid'] ] = event if opening: self.onChannelChange( ami, event, opening = opening ) def onChannelHangup( self, ami, event ): """Handle hangup of an existing channel""" try: del self.channels[ event['uniqueid']] except KeyError, err: log.warn( """Hangup on unknown channel %s""", event ) else: log.debug( """Hangup on channel %s""", event ) self.onChannelChange( ami, event, opening = False ) def onChannelChange( self, ami, event, opening=False ): """Channel count has changed, do something useful like enforcing limits""" if opening and len(self.channels) > self.thresholdCount: log.warn( """Current channel count: %s""", len(self.channels ) ) else: log.info( """Current channel count: %s""", len(self.channels ) ) APPLICATION = utilapplication.UtilApplication() if __name__ == "__main__": logging.basicConfig() tracker = ChannelTracker() reactor.callWhenRunning( tracker.main ) reactor.run() starpy-1.0.1.0.git.20151124/examples/readingdigits.py000066400000000000000000000037451274025253300216720ustar00rootroot00000000000000#! /usr/bin/env python """Read digits from the user in various ways...""" from twisted.internet import reactor, defer from starpy import fastagi, error import logging, time log = logging.getLogger( 'hellofastagi' ) class DialPlan( object ): """Stupid little application to report how many times it's been accessed""" def __init__( self ): self.count = 0 def __call__( self, agi ): """Store the AGI instance for later usage, kick off our operations""" self.agi = agi return self.start() def start( self ): """Begin the dial-plan-like operations""" return self.agi.answer().addCallbacks( self.onAnswered, self.answerFailure ) def answerFailure( self, reason ): """Deal with a failure to answer""" log.warn( """Unable to answer channel %r: %s""", self.agi.variables['agi_channel'], reason.getTraceback(), ) self.agi.finish() def onAnswered( self, resultLine ): """We've managed to answer the channel, yay!""" self.count += 1 return self.agi.wait( 2.0 ).addCallback( self.onWaited ) def onWaited( self, result ): """We've finished waiting, tell the user the number""" return self.agi.sayNumber( self.count, '*' ).addErrback( self.onNumberFailed, ).addCallbacks( self.onFinished, self.onFinished, ) def onFinished( self, resultLine ): """We said the number correctly, hang up on the user""" return self.agi.finish() def onNumberFailed( self, reason ): """We were unable to read the number to the user""" log.warn( """Unable to read number to user on channel %r: %s""", self.agi.variables['agi_channel'], reason.getTraceback(), ) def onHangupFailure( self, reason ): """Failed trying to hang up""" log.warn( """Unable to hang up channel %r: %s""", self.agi.variables['agi_channel'], reason.getTraceback(), ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.DEBUG ) f = fastagi.FastAGIFactory(DialPlan()) reactor.listenTCP(4573, f, 50, '127.0.0.1') # only binding on local interface reactor.run() starpy-1.0.1.0.git.20151124/examples/timestamp.py000066400000000000000000000023521274025253300210510ustar00rootroot00000000000000#! /usr/bin/env python """Provide a trivial date-and-time service""" from twisted.internet import reactor from starpy import fastagi import logging, time log = logging.getLogger( 'dateandtime' ) def testFunction( agi ): """Give time for some time a bit in the future""" log.debug( 'testFunction' ) df = agi.streamFile( 'at-tone-time-exactly' ) def onFailed( reason ): log.error( "Failure: %s", reason.getTraceback()) return None def cleanup( result ): agi.finish() return result def onSaid( resultLine ): """Having introduced, actually read the time""" t = time.time() t2 = t+20.0 df = agi.sayDateTime( t2, format='HM' ) def onDateFinished( resultLine ): # now need to sleep until .5 seconds before the time df = agi.wait( t2-.5-time.time() ) def onDoBeep( result ): df = agi.streamFile( 'beep' ) return df return df.addCallback( onDoBeep ) return df.addCallback( onDateFinished ) return df.addCallback( onSaid ).addErrback( onFailed ).addCallbacks( cleanup, cleanup, ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.INFO ) f = fastagi.FastAGIFactory(testFunction) reactor.listenTCP(4574, f, 50, '127.0.0.1') # only binding on local interface reactor.run() starpy-1.0.1.0.git.20151124/examples/timestampapp.py000066400000000000000000000025501274025253300215520ustar00rootroot00000000000000#! /usr/bin/env python """Provide a trivial date-and-time service""" from twisted.internet import reactor from starpy import fastagi import utilapplication import logging, time log = logging.getLogger( 'dateandtime' ) def testFunction( agi ): """Give time for some time a bit in the future""" log.debug( 'testFunction' ) df = agi.streamFile( 'at-tone-time-exactly' ) def onFailed( reason ): log.error( "Failure: %s", reason.getTraceback()) return None def cleanup( result ): agi.finish() return result def onSaid( resultLine ): """Having introduced, actually read the time""" t = time.time() t2 = t+7.0 df = agi.sayDateTime( t2, format='HMS' ) def onDateFinished( resultLine ): # now need to sleep until .05 seconds before the time df = agi.wait( t2-.05-time.time() ) def onDoBeep( result ): df = agi.streamFile( 'beep' ) return df def waitTwo( result ): return agi.streamFile( 'thank-you-for-calling' ) return df.addCallback( onDoBeep ).addCallback( waitTwo ) return df.addCallback( onDateFinished ) return df.addCallback( onSaid ).addErrback( onFailed ).addCallbacks( cleanup, cleanup, ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.INFO ) APPLICATION = utilapplication.UtilApplication() reactor.callWhenRunning( APPLICATION.agiSpecifier.run, testFunction ) reactor.run() starpy-1.0.1.0.git.20151124/examples/utilapplication.py000066400000000000000000000205251274025253300222510ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """Class providing utility applications with common support code""" from basicproperty import common, propertied, basic, weak from ConfigParser import ConfigParser from starpy import fastagi, manager from twisted.internet import defer, reactor import logging,os log = logging.getLogger( 'app' ) class UtilApplication( propertied.Propertied ): """Utility class providing simple application-level operations FastAGI entry points are waitForCallOn and handleCallsFor, which allow for one-shot and permanant handling of calls for an extension (respectively), and agiSpecifier, which is loaded from configuration file (as specified in self.configFiles). """ amiSpecifier = basic.BasicProperty( "amiSpecifier", """AMI connection specifier for the application see AMISpecifier""", defaultFunction = lambda prop,client: AMISpecifier() ) agiSpecifier = basic.BasicProperty( "agiSpecifier", """FastAGI server specifier for the application see AGISpecifier""", defaultFunction = lambda prop,client: AGISpecifier() ) extensionWaiters = common.DictionaryProperty( "extensionWaiters", """Set of deferreds waiting for incoming extensions""", ) extensionHandlers = common.DictionaryProperty( "extensionHandlers", """Set of permanant callbacks waiting for incoming extensions""", ) configFiles = configFiles=('starpy.conf','~/.starpy.conf') def __init__( self ): """Initialise the application from options in configFile""" self.loadConfigurations() def loadConfigurations( self ): parser = self._loadConfigFiles( self.configFiles ) self._copyPropertiesFrom( parser, 'AMI', self.amiSpecifier ) self._copyPropertiesFrom( parser, 'FastAGI', self.agiSpecifier ) return parser def _loadConfigFiles( self, configFiles ): """Load options from configuration files given (if present)""" parser = ConfigParser( ) filenames = [ os.path.abspath( os.path.expandvars( os.path.expanduser( file ) )) for file in configFiles ] log.info( "Possible configuration files:\n\t%s", "\n\t".join(filenames) or None) filenames = [ file for file in filenames if os.path.isfile(file) ] log.info( "Actual configuration files:\n\t%s", "\n\t".join(filenames) or None) parser.read( filenames ) return parser def _copyPropertiesFrom( self, parser, section, client, properties=None ): """Copy properties from the config-parser's given section into client""" if properties is None: properties = client.getProperties() for property in properties: if parser.has_option( section, property.name ): try: value = parser.get( section, property.name ) setattr( client, property.name, value ) except (TypeError,ValueError,AttributeError,NameError), err: log( """Unable to set property %r of %r to config-file value %r: %s"""%( property.name, client, parser.get( section, property.name, 1), err, )) return client def dispatchIncomingCall( self, agi ): """Handle an incoming call (dispatch to the appropriate registered handler)""" extension = agi.variables['agi_extension'] log.info( """AGI connection with extension: %r""", extension ) try: df = self.extensionWaiters.pop( extension ) except KeyError, err: try: callback = self.extensionHandlers[ extension ] except KeyError, err: try: callback = self.extensionHandlers[ None ] except KeyError, err: log.warn( """Unexpected connection to extension %r: %s""", extension, agi.variables ) agi.finish() return try: return callback( agi ) except Exception, err: log.error( """Failure during callback %s for agi %s: %s""", callback, agi.variables, err ) # XXX return a -1 here else: if not df.called: df.callback( agi ) def waitForCallOn( self, extension, timeout=15 ): """Wait for an AGI call on extension given extension -- string extension for which to wait timeout -- duration in seconds to wait before defer.TimeoutError is returned to the deferred. Note that waiting callback overrides any registered handler; that is, if you register one callback with waitForCallOn and another with handleCallsFor, the first incoming call will trigger the waitForCallOn handler. returns deferred returning connected FastAGIProtocol or an error """ extension = str(extension) log.info( 'Waiting for extension %r for %s seconds', extension, timeout ) df = defer.Deferred( ) self.extensionWaiters[ extension ] = df def onTimeout( ): if not df.called: df.errback( defer.TimeoutError( """Timeout waiting for call on extension: %r"""%(extension,) )) reactor.callLater( timeout, onTimeout ) return df def handleCallsFor( self, extension, callback ): """Register permanant handler for given extension extension -- string extension for which to wait or None to define a default handler (that chosen if there is not explicit handler or waiter) callback -- callback function to be called for each incoming channel to the given extension. Note that waiting callback overrides any registered handler; that is, if you register one callback with waitForCallOn and another with handleCallsFor, the first incoming call will trigger the waitForCallOn handler. returns None """ if extension is not None: extension = str(extension) self.extensionHandlers[ extension ] = callback 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 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) starpy-1.0.1.0.git.20151124/requirements.txt000066400000000000000000000000101274025253300201270ustar00rootroot00000000000000Twisted starpy-1.0.1.0.git.20151124/setup.py000077500000000000000000000025411274025253300163730ustar00rootroot00000000000000#!/usr/bin/env python # # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. from setuptools import setup, find_packages VERSION = '1.0.2' setup( name='starpy', version=VERSION, author='Mike C. Fletcher', author_email='mcfletch@vrplumber.com', description='Twisted Protocols for interaction with the Asterisk PBX', license='BSD', long_description=open('README.rst').read(), keywords='asterisk manager fastagi twisted AMI', url='https://github.com/asterisk/starpy', packages=find_packages(), classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", ], ) starpy-1.0.1.0.git.20151124/starpy/000077500000000000000000000000001274025253300161765ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/starpy/__init__.py000066400000000000000000000011261274025253300203070ustar00rootroot00000000000000"""Twisted Protocols for Communication with the Asterisk PBX StarPy allows you to communicate with an Asterisk PBX using an Asterisk Manager Interface (AMI) client or a Fast Asterisk Gateway Interface (FastAGI) server. The protocols are designed to be included in applications that want to allow for multi-protocol communication using the Twisted protocol. Their integration with Asterisk does not require any modification to the Asterisk source code (though a manager account is obviously required for the AMI interface, and you have to actually call the FastAGI server from the dialplan). """ starpy-1.0.1.0.git.20151124/starpy/error.py000066400000000000000000000022741274025253300177060ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """Collection of StarPy-specific error classes""" class AMICommandFailure(Exception): """AMI Command failure of some description""" class AGICommandFailure(Exception): """AGI Command failure of some description""" class MenuFinished(Exception): """Base class for reporting non-standard menu exits (i.e. not a choice)""" class MenuExit(MenuFinished): """User exited from the menu voluntarily""" class MenuTimeout(MenuFinished): """User didn't complete selection from menu in reasonable time period""" class MenuUnexpectedOption(MenuFinished): """Somehow the user managed to select an option that doesn't exist?""" starpy-1.0.1.0.git.20151124/starpy/fastagi.py000066400000000000000000001115011274025253300201650ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """Asterisk FastAGI server for use from the dialplan You use an asterisk FastAGI like this from extensions.conf: exten => 1000,3,AGI(agi://127.0.0.1:4573,arg1,arg2) Where 127.0.0.1 is the server and 4573 is the port on which the server is listening. Module defines a standard Python logging module log 'FastAGI' """ from twisted.internet import protocol, reactor, defer from twisted.internet import error as tw_error from twisted.protocols import basic import socket import logging import time from starpy import error log = logging.getLogger('FastAGI') FAILURE_CODE = -1 class FastAGIProtocol(basic.LineOnlyReceiver): """Protocol for the interfacing with the Asterisk FastAGI application Attributes: variables -- for connected protocol, the set of variables passed during initialisation, keys are all-lower-case, set of variables returned for an Asterisk 1.2.1 installation on Gentoo on a locally connected channel: agi_network = 'yes' agi_request = 'agi://localhost' agi_channel = 'SIP/mike-ccca' agi_language = 'en' agi_type = 'SIP' agi_uniqueid = '1139871605.0' agi_callerid = 'mike' agi_calleridname = 'Mike Fletcher' agi_callingpres = '0' agi_callingani2 = '0' agi_callington = '0' agi_callingtns = '0' agi_dnid = '1' agi_rdnis = 'unknown' agi_context = 'testing' agi_extension = '1' agi_priority = '1' agi_enhanced = '0.0' agi_accountcode = '' # Internal: readingVariables -- whether the instance is still in initialising by reading the setup variables from the connection messageCache -- stores incoming variables pendingMessages -- set of outstanding messages for which we expect replies lostConnectionDeferred -- deferred firing when the connection is lost delimiter -- uses bald newline instead of carriage-return-newline XXX Lots of problems with data-escaping, no docs on how to escape special characters that I can see... """ readingVariables = False lostConnectionDeferred = None delimiter = '\n' def __init__(self, *args, **named): """Initialise the AMIProtocol, arguments are ignored""" self.messageCache = [] self.variables = {} self.pendingMessages = [] def connectionMade(self): """(Internal) Handle incoming connection (new AGI request) Initiates read of the initial attributes passed by the server """ log.info("New Connection") self.readingVariables = True def connectionLost(self, reason): """(Internal) Handle loss of the connection (remote hangup)""" log.info("""Connection terminated""") try: for df in self.pendingMessages: df.errback(tw_error.ConnectionDone( "FastAGI connection terminated")) finally: if self.lostConnectionDeferred: self.lostConnectionDeferred.errback(reason) del self.pendingMessages[:] def onClose(self): """Return a deferred which will fire when the connection is lost""" if not self.lostConnectionDeferred: self.lostConnectionDeferred = defer.Deferred() return self.lostConnectionDeferred def lineReceived(self, line): """(Internal) Handle Twisted's report of an incoming line from AMI""" log.debug('Line In: %r', line) if self.readingVariables: if not line.strip(): self.readingVariables = False self.factory.mainFunction(self) else: try: key, value = line.split(':', 1) value = value[1:].rstrip('\n').rstrip('\r') except ValueError, err: log.error("""Invalid variable line: %r""", line) else: self.variables[key.lower()] = value log.debug("""%s = %r""", key, value) else: try: df = self.pendingMessages.pop(0) except IndexError, err: log.warn("Line received without pending deferred: %r", line) else: if line.startswith('200'): line = line[4:] if line.lower().startswith('result='): line = line[7:] df.callback(line) else: # XXX parse out the error code try: errCode, line = line.split(' ', 1) errCode = int(errCode) except ValueError, err: errCode = 500 df.errback(error.AGICommandFailure(errCode, line)) def sendCommand(self, commandString): """(Internal) Send the given command to the other side""" log.info("Send Command: %r", commandString) commandString = commandString.rstrip('\n').rstrip('\r') df = defer.Deferred() self.pendingMessages.append(df) self.sendLine(commandString) return df def checkFailure(self, result, failure='-1'): """(Internal) Check for a failure-code, raise error if == result""" # result code may have trailing information... try: resultInt, line = result.split(' ', 1) except ValueError, err: resultInt = result if resultInt.strip() == failure: raise error.AGICommandFailure(FAILURE_CODE, result) return result def resultAsInt(self, result): """(Internal) Convert result to an integer value""" try: return int(result.strip()) except ValueError, err: raise error.AGICommandFailure(FAILURE_CODE, result) def secondResultItem(self, result): """(Internal) Retrieve the second item on the result-line""" return result.split(' ', 1)[1] def resultPlusTimeoutFlag(self, resultLine): """(Internal) Result followed by optional flag declaring timeout""" try: digits, timeout = resultLine.split(' ', 1) return digits.strip(), True except ValueError, err: return resultLine.strip(), False def dateAsSeconds(self, date): """(Internal) Convert date to asterisk-compatible format""" if hasattr(date, 'timetuple'): # XXX values seem to be off here... date = time.mktime(date.timetuple()) elif isinstance(date, time.struct_time): date = time.mktime(date) return date def onRecordingComplete(self, resultLine): """(Internal) Handle putative success Also watch for failure-on-load problems """ try: digit, exitType, endposStuff = resultLine.split(' ', 2) except ValueError, err: pass else: digit = int(digit) exitType = exitType.strip('()') endposStuff = endposStuff.strip() if endposStuff.startswith('endpos='): endpos = int(endposStuff[7:].strip()) return digit, exitType, endpos raise ValueError("Unexpected result on streaming completion: %r" % resultLine) def onStreamingComplete(self, resultLine, skipMS=0): """(Internal) Handle putative success Also watch for failure-on-load problems """ try: digit, endposStuff = resultLine.split(' ', 1) except ValueError, err: pass else: digit = int(digit) endposStuff = endposStuff.strip() if endposStuff.startswith('endpos='): endpos = int(endposStuff[7:].strip()) if endpos == skipMS: # "likely" an error according to the wiki, # we'll raise an error... raise error.AGICommandFailure(FAILURE_CODE, "End position %s == original position, " "result code %s" % (endpos, digit)) return digit, endpos raise ValueError("Unexpected result on streaming completion: %r" % resultLine) def jumpOnError(self, reason, difference=100, forErrors=None): """On error, jump to original priority+100 This is intended to be registered as an errBack on a deferred for an end-user application. It performs the Asterisk-standard-ish jump-on-failure operation, jumping to new priority of priority+difference. It also forces return to the same context and extension, in case some other piece of code has changed those. difference -- priority jump to execute forErrors -- if specified, a tuple of error classes to which this particular jump is limited (i.e. only errors of this type will generate a jump & disconnect) returns deferred from the InSequence of operations required to reset the address... """ if forErrors: if not isinstance(forErrors, (tuple, list)): forErrors = (forErrors,) reason.trap(*forErrors) sequence = InSequence() sequence.append(self.setContext, self.variables['agi_context']) sequence.append(self.setExtension, self.variables['agi_extension']) sequence.append(self.setPriority, int(self.variables['agi_priority']) + difference) sequence.append(self.finish) return sequence() # End-user API def finish(self): """Finish the AGI "script" (drop connection) This command simply drops the connection to the Asterisk server, which the FastAGI protocol interprets as a successful termination. Note: There *should* be a mechanism for sending a "result" code, but I haven't found any documentation for it. """ self.transport.loseConnection() def answer(self): """Answer the channel (go off-hook) Returns deferred integer response code """ return self.sendCommand("ANSWER").addCallback( self.checkFailure ).addCallback(self.resultAsInt) def channelStatus(self, channel=None): """Retrieve the current channel's status Result integers (from the wiki): 0 Channel is down and available 1 Channel is down, but reserved 2 Channel is off hook 3 Digits (or equivalent) have been dialed 4 Line is ringing 5 Remote end is ringing 6 Line is up 7 Line is busy Returns deferred integer result code This could be used to decide if we can forward the channel to a given user, or whether we need to shunt them off somewhere else. """ if channel: command = 'CHANNEL STATUS "%s"' % (channel) else: command = "CHANNEL STATUS" return self.sendCommand(command).addCallback( self.checkFailure, ).addCallback(self.resultAsInt) def onControlStreamFileComplete(self, resultLine): """(Internal) Handle CONTROL STREAM FILE results. Asterisk 12 introduces 'endpos=' to the result line. """ parts = resultLine.split(' ', 1) result = int(parts[0]) endpos = None # Default if endpos isn't specified if len(parts) == 2: endposStuff = parts[1].strip() if endposStuff.startswith('endpos='): endpos = int(endposStuff[7:]) else: log.error("Unexpected response to 'control stream file': %s", resultLine) return result, endpos def controlStreamFile( self, filename, escapeDigits, skipMS=0, ffChar='*', rewChar='#', pauseChar=None, ): """Playback specified file with ability to be controlled by user filename -- filename to play (on the asterisk server) (don't use file-type extension!) escapeDigits -- if provided, skipMS -- number of milliseconds to skip on FF/REW ffChar -- if provided, the set of chars that fast-forward rewChar -- if provided, the set of chars that rewind pauseChar -- if provided, the set of chars that pause playback returns deferred (digit,endpos) on success, or errors on failure, note that digit will be 0 if no digit was pressed AFAICS """ command = 'CONTROL STREAM FILE "%s" %r %s %r %r' % ( filename, escapeDigits, skipMS, ffChar, rewChar ) if pauseChar: command += ' %r' % (pauseChar) return self.sendCommand(command).addCallback(self.checkFailure) \ .addCallback(self.onControlStreamFileComplete) def databaseDel(self, family, key): """Delete the given key from the database Returns deferred integer result code """ command = 'DATABASE DEL "%s" "%s"' % (family, key) return self.sendCommand(command).addCallback( self.checkFailure, failure='0', ).addCallback(self.resultAsInt) def databaseDeltree(self, family, keyTree=None): """Delete an entire family or a tree within a family from database Returns deferred integer result code """ command = 'DATABASE DELTREE "%s"' % (family,) if keyTree: command += ' "%s"' % (keytree,) return self.sendCommand(command).addCallback( self.checkFailure, failure='0', ).addCallback(self.resultAsInt) def databaseGet(self, family, key): """Retrieve value of the given key from database Returns deferred string value for the key """ command = 'DATABASE GET "%s" "%s"' % (family, key) def returnValue(resultLine): # get the second item without the brackets... return resultLine[1:-1] return self.sendCommand(command).addCallback( self.checkFailure, failure='0', ).addCallback(self.secondResultItem).addCallback(returnValue) def databaseSet(self, family, key, value): """Set value of the given key to database a.k.a databasePut on the asterisk side Returns deferred integer result code """ command = 'DATABASE PUT "%s" "%s" "%s"' % (family, key, value) return self.sendCommand(command).addCallback( self.checkFailure, failure='0', ).addCallback(self.resultAsInt) databasePut = databaseSet def execute(self, application, *options, **kwargs): """Execute a dialplan application with given options Note: asterisk calls this "exec", which is Python keyword comma_delimiter -- Use new style comma delimiter for diaplan application arguments. Asterisk uses pipes in 1.4 and older and prefers commas in 1.6 and up. Pass comma_delimiter=True to avoid warnings from Asterisk 1.6 and up. Returns deferred string result for the application, which may have failed, result values are application dependant. """ command = '''EXEC "%s"''' % (application) if options: if kwargs.pop('comma_delimiter', False) is True: delimiter = "," else: delimiter = "|" command += ' "%s"' % ( delimiter.join([ str(x) for x in options ]) ) return self.sendCommand(command).addCallback( self.checkFailure, failure='-2', ) def getData(self, filename, timeout=2.000, maxDigits=None): """Playback file, collecting up to maxDigits or waiting up to timeout filename -- filename without extension to play timeout -- timeout in seconds (Asterisk uses milliseconds) maxDigits -- maximum number of digits to collect returns deferred (str(digits), bool(timedOut)) """ timeout *= 1000 command = '''GET DATA "%s" %s''' % (filename, timeout) if maxDigits is not None: command = ' '.join([command, str(maxDigits)]) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultPlusTimeoutFlag) def getOption(self, filename, escapeDigits, timeout=None): """Playback file, collect 1 digit or timeout (return 0) filename -- filename to play escapeDigits -- digits which cancel playback/recording timeout -- timeout in seconds (Asterisk uses milliseconds) returns (chr(option) or '' on timeout, endpos) """ command = '''GET OPTION "%s" %r''' % (filename, escapeDigits) if timeout is not None: timeout *= 1000 command += ' %s' % (timeout,) def charFirst((c, position)): if not c: # returns 0 on timeout c = '' else: c = chr(c) return c, position return self.sendCommand(command).addCallback( self.checkFailure, ).addCallback( self.onStreamingComplete ).addCallback(charFirst) def getVariable(self, variable): """Retrieve the given channel variable From the wiki, variables of interest: ACCOUNTCODE -- Account code, if specified ANSWEREDTIME -- Time call was answered BLINDTRANSFER -- Active SIP channel that dialed the number. This will return the SIP Channel that dialed the number when doing blind transfers CALLERID -- Current Caller ID (name and number) # deprecated? CALLINGPRES -- PRI Call ID Presentation variable for incoming calls CHANNEL -- Current channel name CONTEXT -- Current context name DATETIME -- Current datetime in format: DDMMYYYY-HH:MM:SS DIALEDPEERNAME -- Name of called party (Broken) DIALEDPEERNUMBER -- Number of the called party (Broken) DIALEDTIME -- Time number was dialed DIALSTATUS -- Status of the call DNID -- Dialed Number Identifier (limited apparently) EPOCH -- UNIX-style epoch-based time (seconds since 1 Jan 1970) EXTEN -- Current extension HANGUPCAUSE -- Last hangup return code on a Zap channel connected to a PRI interface INVALID_EXTEN -- Extension asked for when redirected to the i (invalid) extension LANGUAGE -- The current language setting. See Asterisk multi-language MEETMESECS -- Number of seconds user participated in a MeetMe conference PRIORITY -- Current priority RDNIS -- The current redirecting DNIS, Caller ID that redirected the call. Limitations apply. SIPDOMAIN -- SIP destination domain of an inbound call (if appropriate) SIP_CODEC -- Used to set the SIP codec for a call (apparently broken in Ver 1.0.1, ok in Ver. 1.0.3 & 1.0.4, not sure about 1.0.2) SIPCALLID -- SIP dialog Call-ID: header SIPUSERAGENT -- SIP user agent header (remote agent) TIMESTAMP -- Current datetime in the format: YYYYMMDD-HHMMSS TXTCIDNAME -- Result of application TXTCIDName UNIQUEID -- Current call unique identifier TOUCH_MONITOR -- Used for "one touch record" (see features.conf, and wW dial flags). If is set on either side of the call then that var contains the app_args for app_monitor otherwise the default of WAV||m is used Returns deferred string value for the key """ def stripBrackets(value): return value.strip()[1:-1] command = '''GET VARIABLE "%s"''' % (variable,) return self.sendCommand(command).addCallback( self.checkFailure, failure='0', ).addCallback(self.secondResultItem).addCallback(stripBrackets) def hangup(self, channel=None): """Cause the server to hang up on the channel Returns deferred integer response code Note: This command just doesn't seem to work with Asterisk 1.2.1, connected channels just remain connected. """ command = "HANGUP" if channel is not None: command += ' "%s"' % (channel) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def noop(self, message=None): """Send a null operation to the server. Any message sent will be printed to the CLI. Returns deferred integer response code """ command = "NOOP" if message is not None: command += ' "%s"' % message return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def playback(self, filename, doAnswer=1): """Playback specified file in foreground filename -- filename to play doAnswer -- whether to: -1: skip playback if the channel is not answered 0: playback the sound file without answering first 1: answer the channel before playback, if not yet answered Note: this just wraps the execute method to issue a PLAYBACK command. Returns deferred integer response code """ try: option = {-1: 'skip', 0: 'noanswer', 1: 'answer'}[doAnswer] except KeyError: raise TypeError("doAnswer accepts values -1, 0, " "1 only (%s given)" % doAnswer) command = 'PLAYBACK "%s"' % (filename,) if option: command += ' "%s"' % (option,) return self.execute(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def receiveChar(self, timeout=None): """Receive a single text char on text-supporting channels (rare) timeout -- timeout in seconds (Asterisk uses milliseconds) returns deferred (char, bool(timeout)) """ command = '''RECEIVE CHAR''' if timeout is not None: timeout *= 1000 command += ' %s' % (timeout,) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultPlusTimeoutFlag) def receiveText(self, timeout=None): """Receive text until timeout timeout -- timeout in seconds (Asterisk uses milliseconds) Returns deferred string response value (unaltered) """ command = '''RECEIVE TEXT''' if timeout is not None: timeout *= 1000 command += ' %s' % (timeout,) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ) def recordFile( self, filename, format, escapeDigits, timeout=-1, offsetSamples=None, beep=True, silence=None, ): """Record channel to given filename until escapeDigits or silence filename -- filename on the server to which to save format -- encoding format in which to save data escapeDigits -- digits which end recording timeout -- maximum time to record in seconds, -1 gives infinite (Asterisk uses milliseconds) offsetSamples - move into file this number of samples before recording? XXX check semantics here. beep -- if true, play a Beep on channel to indicate start of recording silence -- if specified, silence duration to trigger end of recording returns deferred (str(code/digits), typeOfExit, endpos) Where known typeOfExits include: hangup, code='0' dtmf, code=digits-pressed timeout, code='0' """ timeout *= 1000 command = '''RECORD FILE "%s" "%s" %s %s''' % ( filename, format, escapeDigits, timeout, ) if offsetSamples is not None: command += ' %s' % (offsetSamples,) if beep: command += ' BEEP' if silence is not None: command += ' s=%s' % (silence,) def onResult(resultLine): value, type, endpos = resultLine.split(' ') type = type.strip()[1:-1] endpos = int(endpos.split('=')[1]) return (value, type, endpos) return self.sendCommand(command).addCallback( self.onRecordingComplete ) def sayXXX(self, baseCommand, value, escapeDigits=''): """Underlying implementation for the common-api sayXXX functions""" command = '%s %s %r' % (baseCommand, value, escapeDigits or '') return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def sayAlpha(self, string, escapeDigits=None): """Spell out character string to the user until escapeDigits returns deferred 0 or the digit pressed """ string = "".join([x for x in string if x.isalnum()]) return self.sayXXX('SAY ALPHA', string, escapeDigits) def sayDate(self, date, escapeDigits=None): """Spell out the date (with somewhat unnatural form) See sayDateTime with format 'ABdY' for a more natural reading returns deferred 0 or digit-pressed as integer """ return self.sayXXX('SAY DATE', self.dateAsSeconds(date), escapeDigits) def sayDigits(self, number, escapeDigits=None): """Spell out the number/string as a string of digits returns deferred 0 or digit-pressed as integer """ number = "".join([x for x in str(number) if x.isdigit()]) return self.sayXXX('SAY DIGITS', number, escapeDigits) def sayNumber(self, number, escapeDigits=None): """Say a number in natural form returns deferred 0 or digit-pressed as integer """ number = "".join([x for x in str(number) if x.isdigit()]) return self.sayXXX('SAY NUMBER', number, escapeDigits) def sayPhonetic(self, string, escapeDigits=None): """Say string using phonetics returns deferred 0 or digit-pressed as integer """ string = "".join([x for x in string if x.isalnum()]) return self.sayXXX('SAY PHONETIC', string, escapeDigits) def sayTime(self, time, escapeDigits=None): """Say string using phonetics returns deferred 0 or digit-pressed as integer """ return self.sayXXX('SAY TIME', self.dateAsSeconds(time), escapeDigits) def sayDateTime(self, time, escapeDigits='', format=None, timezone=None): """Say given date/time in given format until escapeDigits time -- datetime or float-seconds-since-epoch escapeDigits -- digits to cancel playback format -- strftime-style format for the date to be read 'filename' -- filename of a soundfile (single ticks around the filename required) A or a -- Day of week (Saturday, Sunday, ...) B or b or h -- Month name (January, February, ...) d or e -- numeric day of month (first, second, ..., thirty-first) Y -- Year I or l -- Hour, 12 hour clock H -- Hour, 24 hour clock (single digit hours preceded by "oh") k -- Hour, 24 hour clock (single digit hours NOT preceded by "oh") M -- Minute P or p -- AM or PM Q -- "today", "yesterday" or ABdY (*note: not standard strftime value) q -- "" (for today), "yesterday", weekday, or ABdY (*note: not standard strftime value) R -- 24 hour time, including minute Default format is "ABdY 'digits/at' IMp" timezone -- optional timezone name from /usr/share/zoneinfo returns deferred 0 or digit-pressed as integer """ command = 'SAY DATETIME %s %r' % (self.dateAsSeconds(time), escapeDigits) if format is not None: command += ' %s' % (format,) if timezone is not None: command += ' %s' % (timezone,) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def sendImage(self, filename): """Send image on those channels which support sending images (rare) returns deferred integer result code """ command = 'SEND IMAGE "%s"' % (filename,) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def sendText(self, text): """Send text on text-supporting channels (rare) returns deferred integer result code """ command = "SEND TEXT %r" % (text) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def setAutoHangup(self, time): """Set channel to automatically hang up after time seconds time -- time in seconds in the future to hang up... returns deferred integer result code """ command = """SET AUTOHANGUP %s""" % (time,) return self.sendCommand(command).addCallback( # docs don't show a failure case, actually self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def setCallerID(self, number): """Set channel's caller ID to given number returns deferred integer result code """ command = "SET CALLERID %s" % (number) return self.sendCommand(command).addCallback(self.resultAsInt) def setContext(self, context): """Move channel to given context (no error checking is performed) returns deferred integer result code """ command = """SET CONTEXT %s""" % (context,) return self.sendCommand(command).addCallback(self.resultAsInt) def setExtension(self, extension): """Move channel to given extension (or 'i' if invalid) The call will drop if neither the extension or 'i' are there. returns deferred integer result code """ command = """SET EXTENSION %s""" % (extension,) return self.sendCommand(command).addCallback(self.resultAsInt) def setMusic(self, on=True, musicClass=None): """Enable/disable and/or choose music class for channel's music-on-hold returns deferred integer result code """ command = """SET MUSIC %s""" % (['OFF', 'ON'][on],) if musicClass is not None: command += " %s" % (musicClass,) return self.sendCommand(command).addCallback(self.resultAsInt) def setPriority(self, priority): """Move channel to given priority or drop if not there returns deferred integer result code """ command = """SET PRIORITY %s""" % (priority,) return self.sendCommand(command).addCallback(self.resultAsInt) def setVariable(self, variable, value): """Set given channel variable to given value variable -- the variable name passed to the server value -- the variable value passed to the server, will have any '"' characters removed in order to allow for " quoting of the value. returns deferred integer result code """ value = '''"%s"''' % (str(value).replace('"', ''),) command = 'SET VARIABLE "%s" "%s"' % (variable, value) return self.sendCommand(command).addCallback(self.resultAsInt) def streamFile(self, filename, escapeDigits="", offset=0): """Stream given file until escapeDigits starting from offset returns deferred (str(digit), int(endpos)) for playback Note: streamFile is apparently unstable in AGI, may want to use execute('PLAYBACK', ...) instead (according to the Wiki) """ command = 'STREAM FILE "%s" %r' % (filename, escapeDigits) if offset is not None: command += ' %s' % (offset) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.onStreamingComplete, skipMS=offset) def tddMode(self, on=True): """Set TDD mode on the channel if possible (ZAP only ATM) on -- ON (True), OFF (False) or MATE (None) returns deferred integer result code """ if on is True: on = 'ON' elif on is False: on = 'OFF' elif on is None: on = 'MATE' command = 'TDD MODE %s' % (on,) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', # failure ).addCallback( # planned eventual failure case (not capable) self.checkFailure, failure='0', ).addCallback( self.resultAsInt, ) def verbose(self, message, level=None): """Send a logging message to the asterisk console for debugging etc message -- text to pass level -- 1-4 denoting verbosity level returns deferred integer result code """ command = 'VERBOSE %r' % (message,) if level is not None: command += ' %s' % (level) return self.sendCommand(command).addCallback( self.resultAsInt, ) def waitForDigit(self, timeout): """Wait up to timeout seconds for single digit to be pressed timeout -- timeout in seconds or -1 for infinite timeout (Asterisk uses milliseconds) returns deferred 0 on timeout or digit """ timeout *= 1000 command = "WAIT FOR DIGIT %s" % (timeout,) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback( self.resultAsInt, ) def wait(self, duration): """Wait for X seconds (just a wrapper around callLater, doesn't talk to server) returns deferred which fires some time after duration seconds have passed """ df = defer.Deferred() reactor.callLater(duration, df.callback, 0) return df class InSequence(object): """Single-shot item creating a set of actions to run in sequence""" def __init__(self): self.actions = [] self.results = [] self.finalDF = None def append(self, function, *args, **named): """Append an action to the set of actions to process""" self.actions.append((function, args, named)) def __call__(self): """Return deferred that fires when finished processing all items""" return self._doSequence() def _doSequence(self): """Return a deferred that does each action in sequence""" finalDF = defer.Deferred() self.onActionSuccess(None, finalDF=finalDF) return finalDF def recordResult(self, result): """Record the result for later""" self.results.append(result) return result def onActionSuccess(self, result, finalDF): """Handle individual-action success""" log.debug('onActionSuccess: %s', result) if self.actions: action = self.actions.pop(0) log.debug('action %s', action) df = defer.maybeDeferred(action[0], *action[1], **action[2]) df.addCallback(self.recordResult) df.addCallback(self.onActionSuccess, finalDF=finalDF) df.addErrback(self.onActionFailure, finalDF=finalDF) return df else: finalDF.callback(self.results) def onActionFailure(self, reason, finalDF): """Handle individual-action failure""" log.debug('onActionFailure') reason.results = self.results finalDF.errback(reason) class FastAGIFactory(protocol.Factory): """Factory generating FastAGI server instances """ protocol = FastAGIProtocol def __init__(self, mainFunction): """Initialise the factory mainFunction -- function taking a connected FastAGIProtocol instance this is the function that's run when the Asterisk server connects. """ self.mainFunction = mainFunction starpy-1.0.1.0.git.20151124/starpy/manager.py000066400000000000000000001152341274025253300201700ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """Asterisk Manager Interface for the Twisted networking framework The Asterisk Manager Interface is a simple line-oriented protocol that allows for basic control of the channels active on a given Asterisk server. Module defines a standard Python logging module log 'AMI' """ from twisted.internet import protocol, reactor, defer from twisted.protocols import basic from twisted.internet import error as tw_error import socket import logging from hashlib import md5 from starpy import error log = logging.getLogger('AMI') class deferredErrorResp(defer.Deferred): """A subclass of defer.Deferred that adds a registerError method to handle function callback when an Error response happens""" _errorRespCallback = None def registerError(self, function ): """Add function for Error response callback""" self._errorRespCallback = function log.debug('Registering function %s to handle Error response' % (function)) class AMIProtocol(basic.LineOnlyReceiver): """Protocol for the interfacing with the Asterisk Manager Interface (AMI) Provides most of the AMI Action interfaces. Auto-generates ActionID fields for all calls. Events and messages are passed around as simple dictionaries with all-lowercase keys. Values are case-sensitive. XXX Want to allow for timeouts Attributes: count -- total count of messages sent from this protocol hostName -- used along with count and ID to produce unique IDs messageCache -- stores incoming message fragments from the manager id -- An identifier for this instance """ count = 0 amiVersion = None id = None def __init__(self, *args, **named): """Initialise the AMIProtocol, arguments are ignored""" self.messageCache = [] self.actionIDCallbacks = {} self.eventTypeCallbacks = {} self.hostName = socket.gethostname() def registerEvent(self, event, function): """Register callback for the given event-type event -- string name for the event, None to match all events, or a tuple of string names to match multiple events. See http://www.voip-info.org/wiki/view/asterisk+manager+events for list of events and the data they bear. Includes: Newchannel -- note that you can receive multiple Newchannel events for a single channel! Hangup Newexten Newstate Reload Shutdown ExtensionStatus Rename Newcallerid Alarm AlarmClear Agentcallbacklogoff Agentcallbacklogin Agentlogin Agentlogoff MeetmeJoin MeetmeLeave MessageWaiting Join Leave AgentCalled ParkedCall UnParkedCall ParkedCalls Cdr ParkedCallsComplete QueueParams QueueMember among other standard events. Also includes user-defined events. function -- function taking (protocol,event) as arguments or None to deregister the current function. Multiple functions may be registered for a given event """ log.debug('Registering function %s to handle events of type %r', function, event) if isinstance(event, (str, unicode, type(None))): event = (event,) for ev in event: self.eventTypeCallbacks.setdefault(ev, []).append(function) def deregisterEvent(self, event, function=None): """Deregister callback for the given event-type event -- event name (or names) to be deregistered, see registerEvent function -- the function to be removed from the callbacks or None to remove all callbacks for the event returns success boolean """ log.debug('Deregistering handler %s for events of type %r', function, event) if isinstance(event, (str, unicode, type(None))): event = (event,) success = True for ev in event: try: set = self.eventTypeCallbacks[ev] except KeyError, err: success = False else: try: while function in set: set.remove(function) except (ValueError, KeyError), err: success = False if not set or function is None: try: del self.eventTypeCallbacks[ev] except KeyError, err: success = False return success def lineReceived(self, line): """Handle Twisted's report of an incoming line from the manager""" log.debug('Line In: %r', line) self.messageCache.append(line) if not line.strip(): self.dispatchIncoming() # does dispatch and clears cache def connectionMade(self): """Handle connection to the AMI port (auto-login) This is a Twisted customisation point, we use it to automatically log into the connection we've just established. XXX Should probably use proper Twisted-style credential negotiations """ log.info('Connection Made') self.factory.resetDelay() if self.factory.plaintext_login: df = self.login() else: df = self.loginChallengeResponse() def onComplete(message): """Check for success, errback or callback as appropriate""" if not message['response'] == 'Success': log.info('Login Failure: %s', message) self.transport.loseConnection() self.factory.loginDefer.errback( error.AMICommandFailure("Unable to connect to manager", message) ) else: # XXX messy here, would rather have the factory trigger its own # callback... log.info('Login Complete: %s', message) self.factory.loginDefer.callback( self, ) def onFailure(reason): """Handle failure to connect (e.g. due to timeout)""" log.info('Login Call Failure: %s', reason.getTraceback()) self.transport.loseConnection() self.factory.loginDefer.errback( reason ) df.addCallbacks(onComplete, onFailure) def connectionLost(self, reason): """Connection lost, clean up callbacks""" for key, callable in self.actionIDCallbacks.items(): try: callable(tw_error.ConnectionDone( "FastAGI connection terminated")) except Exception, err: log.error("Failure during connectionLost for callable %s: %s", callable, err) self.actionIDCallbacks.clear() self.eventTypeCallbacks.clear() VERSION_PREFIX = 'Asterisk Call Manager' END_DATA = '--END COMMAND--' def dispatchIncoming(self): """Dispatch any finished incoming events/messages""" log.debug('Dispatch Incoming') message = {} while self.messageCache: line = self.messageCache.pop(0) line = line.strip() if line: if line.endswith(self.END_DATA): # multi-line command results... message.setdefault(' ', []).extend([ l for l in line.split('\n') if (l and l != self.END_DATA) ]) else: # regular line... if line.startswith(self.VERSION_PREFIX): self.amiVersion = line[ len(self.VERSION_PREFIX) + 1:].strip() else: try: key, value = line.split(':', 1) except ValueError, err: # XXX data-safety issues, what prevents the # VERSION_PREFIX from showing up in a data-set? log.warn("Improperly formatted line received and " "ignored: %r", line) else: message[key.lower().strip()] = value.strip() log.debug('Incoming Message: %s', message) if 'actionid' in message: key = message['actionid'] callback = self.actionIDCallbacks.get(key) if callback: try: callback(message) except Exception, err: # XXX log failure here... pass # otherwise is a monitor message or something we didn't send... if 'event' in message: self.dispatchEvent(message) def dispatchEvent(self, event): """Given an incoming event, dispatch to registered handlers""" for key in (event['event'], None): try: handlers = self.eventTypeCallbacks[key] except KeyError, err: pass else: for handler in handlers: try: handler(self, event) except Exception, err: # would like the getException code here... log.error( 'Exception in event handler %s on event %s: %s', handler, event, err ) def generateActionId(self): """Generate a unique action ID Assumes that hostName must be unique among all machines which talk to a given AMI server. With that is combined the memory location of the protocol object (which should be machine-unique) and the count of messages that this manager has created so far. Generally speaking, you shouldn't need to know the action ID, as the protocol handles the management of them automatically. """ self.count += 1 return '%s-%s-%s' % (self.hostName, id(self), self.count) def sendDeferred(self, message): """Send with a single-callback deferred object Returns deferred that fires when a response to this message is received """ df = deferredErrorResp() actionid = self.sendMessage(message, df.callback) df.addCallbacks( self.checkErrorResponse, self.cleanup, callbackArgs=(actionid, df,), errbackArgs=(actionid,) ) return df def checkErrorResponse(self, result, actionid, df): """Check for error response and callback""" self.cleanup( result, actionid) if isinstance(result, dict) and result.get('response') == 'Error' and df._errorRespCallback: df._errorRespCallback(result) return result def cleanup(self, result, actionid): """Cleanup callbacks on completion""" try: del self.actionIDCallbacks[actionid] except KeyError, err: pass return result def sendMessage(self, message, responseCallback=None): """Send the message to the other side, return deferred for the result returns the actionid for the message """ if type(message) == list: actionid = next((value for header, value in message if str(header.lower()) == 'actionid'), None) if actionid is None: actionid = self.generateActionId() message.append(['actionid', str(actionid)]) if responseCallback: self.actionIDCallbacks[actionid] = responseCallback log.debug("""MSG OUT: %s""", message) for item in message: self.sendLine('%s: %s' % (str(item[0].lower()), str(item[1]))) else: message = dict([(k.lower(), v) for (k, v) in message.items()]) if 'actionid' not in message: message['actionid'] = self.generateActionId() if responseCallback: self.actionIDCallbacks[message['actionid']] = responseCallback log.debug("""MSG OUT: %s""", message) for key, value in message.items(): self.sendLine('%s: %s' % (str(key.lower()), str(value))) self.sendLine('') if type(message) == list: return actionid else: return message['actionid'] def collectDeferred(self, message, stopEvent): """Collect all responses to this message until stopEvent or error returns deferred returning sequence of events/responses """ df = defer.Deferred() cache = [] def onEvent(event): if event.get('response') == 'Error': df.errback(error.AMICommandFailure(event)) elif event.get('event') == stopEvent: cache.append(event) df.callback(cache) else: cache.append(event) actionid = self.sendMessage(message, onEvent) df.addCallbacks( self.cleanup, self.cleanup, callbackArgs=(actionid,), errbackArgs=(actionid,) ) return df def errorUnlessResponse(self, message, expected='Success'): """Raise AMICommandFailure error unless message['response'] == expected If == expected, returns the message """ if type(message) is dict and message['response'] != expected: raise error.AMICommandFailure(message) return message ## End-user API def absoluteTimeout(self, channel, timeout): """Set timeout value for the given channel (in seconds)""" message = { 'action': 'absolutetimeout', 'timeout': timeout, 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def agentLogoff(self, agent, soft): """Logs off the specified agent for the queue system.""" if soft in (True, 'yes', 1): soft = 'true' else: soft = 'false' message = { 'Action': 'AgentLogoff', 'Agent': agent, 'Soft': soft } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def agents(self): """Retrieve agents information""" message = { "action": "agents" } return self.collectDeferred(message, "AgentsComplete") def changeMonitor(self, channel, filename): """Change the file to which the channel is to be recorded""" message = { 'action': 'changemonitor', 'channel': channel, 'filename': filename } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def command(self, command): """Run asterisk CLI command, return deferred result for list of lines returns deferred returning list of lines (strings) of the command output. See listCommands to see available commands """ message = { 'action': 'command', 'command': command } df = self.sendDeferred(message) df.addCallback(self.errorUnlessResponse, expected='Follows') def onResult(message): if not isinstance(message, dict): return message return message[' '] return df.addCallback(onResult) def action(self, action, **action_args): """Sends an arbitrary action to the AMI""" #action_args will be ar least an empty dict so we build the message from it. action_args['action'] = action return self.sendDeferred(action_args).addCallback(self.errorUnlessResponse) def dbDel(self, family, key): """Delete key value in the AstDB database""" message = { 'Action': 'DBDel', 'Family': family, 'Key': key } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dbDelTree(self, family, key=None): """Delete key value or key tree in the AstDB database""" message = { 'Action': 'DBDelTree', 'Family': family } if key is not None: message['Key'] = key return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dbGet(self, family, key): """This action retrieves a value from the AstDB database""" df = defer.Deferred() def extractValue(ami, event): value = event['val'] self.deregisterEvent("DBGetResponse", extractValue) return df.callback(value) def errorResponse( message ): self.deregisterEvent("DBGetResponse", extractValue) return df.callback(None) message = { 'Action': 'DBGet', 'family': family, 'key': key } self.sendDeferred(message).registerError(errorResponse) self.registerEvent("DBGetResponse", extractValue) return df def dbPut(self, family, key, value): """Sets a key value in the AstDB database""" message = { 'Action': 'DBPut', 'Family': family, 'Key': key, 'Val': value } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def events(self, eventmask=False): """Determine whether events are generated""" if eventmask in ('off', False, 0): eventmask = 'off' elif eventmask in ('on', True, 1): eventmask = 'on' # otherwise is likely a type-mask message = { 'action': 'events', 'eventmask': eventmask } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def extensionState(self, exten, context): """Get extension state This command reports the extension state for the given extension. If the extension has a hint, this will report the status of the device connected to the extension. The following are the possible extension states: -2 Extension removed -1 Extension hint not found 0 Idle 1 In use 2 Busy""" message = { 'Action': 'ExtensionState', 'Exten': exten, 'Context': context } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def getConfig(self, filename): """Retrieves the data from an Asterisk configuration file""" message = { 'Action': 'GetConfig', 'filename': filename } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def getVar(self, channel, variable): """Retrieve the given variable from the channel. If channel is None, this gets a global variable.""" def extractVariable(message): """When message comes in, extract the variable from it""" if variable.lower() in message: value = message[variable.lower()] elif 'value' in message: value = message['value'] else: raise error.AMICommandFailure(message) if value == '(null)': value = None return value message = { 'action': 'getvar', 'variable': variable } # channel is optional if channel: message['channel'] = channel return self.sendDeferred( message ).addCallback( self.errorUnlessResponse ).addCallback( extractVariable, ) def hangup(self, channel): """Tell channel to hang up""" message = { 'action': 'hangup', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def login(self): """Log into the AMI interface (done automatically on connection) Uses factory.username and factory.secret """ self.id = self.factory.id return self.sendDeferred({ 'action': 'login', 'username': self.factory.username, 'secret': self.factory.secret, }).addCallback(self.errorUnlessResponse) def loginChallengeResponse(self): """Log into the AMI interface with challenge-response. Follows the same approach as self.login() using factory.username and factory.secret. Also done automatically on connection: will be called instead of self.login() if factory.plaintext_login is False: see AMIFactory constructor. """ def sendResponse(challenge): if not type(challenge) is dict or not 'challenge' in challenge: raise error.AMICommandFailure(challenge) key_value = md5('%s%s' % (challenge['challenge'], self.factory.secret)).hexdigest() return self.sendDeferred({ 'action': 'Login', 'authtype': 'MD5', 'username': self.factory.username, 'key': key_value, }).addCallback(self.errorUnlessResponse) self.id = self.factory.id return self.sendDeferred({ 'action': 'Challenge', 'authtype': 'MD5', }).addCallback(sendResponse) def listCommands(self): """List the set of commands available Returns a single message with each command-name as a key """ message = { 'action': 'listcommands' } def removeActionId(message): try: del message['actionid'] except KeyError, err: pass return message return self.sendDeferred(message).addCallback( self.errorUnlessResponse ).addCallback( removeActionId ) def logoff(self): """Log off from the manager instance""" message = { 'action': 'logoff' } return self.sendDeferred(message).addCallback( self.errorUnlessResponse, expected='Goodbye', ) def mailboxCount(self, mailbox): """Get count of messages in the given mailbox""" message = { 'action': 'mailboxcount', 'mailbox': mailbox } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def mailboxStatus(self, mailbox): """Get status of given mailbox""" message = { 'action': 'mailboxstatus', 'mailbox': mailbox } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def meetmeMute(self, meetme, usernum): """Mute a user in a given meetme""" message = { 'action': 'MeetMeMute', 'meetme': meetme, 'usernum': usernum } return self.sendDeferred(message) def meetmeUnmute(self, meetme, usernum): """ Unmute a specified user in a given meetme""" message = { 'action': 'meetmeunmute', 'meetme': meetme, 'usernum': usernum } return self.sendDeferred(message) def monitor(self, channel, file, format, mix): """Record given channel to a file (or attempt to anyway)""" message = { 'action': 'monitor', 'channel': channel, 'file': file, 'format': format, 'mix': mix } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def originate( self, channel, context=None, exten=None, priority=None, timeout=None, callerid=None, account=None, application=None, data=None, variable={}, async=False, channelid=None, otherchannelid=None ): """Originate call to connect channel to given context/exten/priority channel -- the outgoing channel to which will be dialed context/exten/priority -- the dialplan coordinate to which to connect the channel (i.e. where to start the called person) timeout -- duration before timeout in seconds (note: not Asterisk standard!) callerid -- callerid to display on the channel account -- account to which the call belongs application -- alternate application to Dial to use for outbound dial data -- data to pass to application variable -- variables associated to the call async -- make the origination asynchronous """ message = [(k, v) for (k, v) in ( ('action', 'originate'), ('channel', channel), ('context', context), ('exten', exten), ('priority', priority), ('callerid', callerid), ('account', account), ('application', application), ('data', data), ('async', str(async)), ('channelid', channelid), ('otherchannelid', otherchannelid), ) if v is not None] if timeout is not None: message.append(('timeout', timeout*1000)) for var_name, var_value in variable.items(): message.append(('variable', '%s=%s' % (var_name, var_value))) return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def park(self, channel, channel2, timeout): """Park channel""" message = { 'action': 'park', 'channel': channel, 'channel2': channel2, 'timeout': timeout } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def parkedCall(self): """Check for a ParkedCall event""" message = { 'action': 'ParkedCall' } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def unParkedCall(self): """Check for an UnParkedCall event """ message = { 'action': 'UnParkedCall' } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def parkedCalls(self): """Retrieve set of parked calls via multi-event callback""" message = { 'action': 'ParkedCalls' } return self.collectDeferred(message, 'ParkedCallsComplete') def pauseMonitor(self, channel): """Temporarily stop recording the channel""" message = { 'action': 'pausemonitor', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def ping(self): """Check to see if the manager is alive...""" message = { 'action': 'ping' } if self.amiVersion == "1.0": return self.sendDeferred(message).addCallback( self.errorUnlessResponse, expected='Pong', ) else: return self.sendDeferred(message).addCallback( self.errorUnlessResponse ) def playDTMF(self, channel, digit): """Play DTMF on a given channel""" message = { 'action': 'playdtmf', 'channel': channel, 'digit': digit } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def queueAdd(self, queue, interface, penalty=0, paused=True, membername=None, stateinterface=None): """Add given interface to named queue""" if paused in (True, 'true', 1): paused = 'true' else: paused = 'false' message = { 'action': 'queueadd', 'queue': queue, 'interface': interface, 'penalty': penalty, 'paused': paused } if membername is not None: message['membername'] = membername if stateinterface is not None: message['stateinterface'] = stateinterface return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def queueLog(self, queue, event, uniqueid=None, interface=None, msg=None): """Adds custom entry in queue_log""" message = { 'action': 'queuelog', 'queue': queue, 'event': event } if uniqueid is not None: message['uniqueid'] = uniqueid if interface is not None: message['interface'] = interface if msg is not None: message['message'] = msg return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def queuePause(self, queue, interface, paused=True, reason=None): if paused in (True, 'true', 1): paused = 'true' else: paused = 'false' message = { 'action': 'queuepause', 'queue': queue, 'interface': interface, 'paused': paused } if reason is not None: message['reason'] = reason return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def queuePenalty(self, interface, penalty, queue=None): """Set penalty for interface""" message = { 'action': 'queuepenalty', 'interface': interface, 'penalty': penalty } if queue is not None: message.update({'queue': queue}) return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def queueRemove(self, queue, interface): """Remove given interface from named queue""" message = { 'action': 'queueremove', 'queue': queue, 'interface': interface } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def queues(self): """Retrieve information about active queues via multiple events""" # XXX AMI returns improperly formatted lines so this doesn't work now. message = { 'action': 'queues' } #return self.collectDeferred(message, 'QueueStatusEnd') return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def queueStatus(self, queue=None, member=None): """Retrieve information about active queues via multiple events""" message = { 'action': 'queuestatus' } if queue is not None: message.update({'queue': queue}) if member is not None: message.update({'member': member}) return self.collectDeferred(message, 'QueueStatusComplete') def redirect(self, channel, context, exten, priority, extraChannel=None): """Transfer channel(s) to given context/exten/priority""" message = { 'action': 'redirect', 'channel': channel, 'context': context, 'exten': exten, 'priority': priority, } if extraChannel is not None: message['extrachannel'] = extraChannel return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def setCDRUserField(self, channel, userField, append=True): """Set/add to a user field in the CDR for given channel""" if append in (True, 'true', 1): append = 'true' else: append = 'false' message = { 'channel': channel, 'userfield': userField, 'append': append, } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def setVar(self, channel, variable, value): """Set channel variable to given value. If channel is None, this sets a global variable.""" message = { 'action': 'setvar', 'variable': variable, 'value': value } # channel is optional if channel: message['channel'] = channel return self.sendDeferred( message ).addCallback( self.errorUnlessResponse ) def sipPeers(self): """List all known sip peers""" # XXX not available on my box... message = { 'action': 'sippeers' } return self.collectDeferred(message, 'PeerlistComplete') def sipShowPeers(self, peer): message = { 'action': 'sipshowpeer', 'peer': peer } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def status(self, channel=None): """Retrieve status for the given (or all) channels The results come in via multi-event callback channel -- channel name or None to retrieve all channels returns deferred returning list of Status Events for each requested channel """ message = { 'action': 'Status' } if channel: message['channel'] = channel return self.collectDeferred(message, 'StatusComplete') def stopMonitor(self, channel): """Stop monitoring the given channel""" message = { 'action': 'monitor', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def unpauseMonitor(self, channel): """Resume recording a channel""" message = { 'action': 'unpausemonitor', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def updateConfig(self, srcfile, dstfile, reload, headers={}): """Update a configuration file headers should be a dictionary with the following keys Action-XXXXXX Cat-XXXXXX Var-XXXXXX Value-XXXXXX Match-XXXXXX """ message = {} if reload in (True, 'yes', 1): reload = 'yes' else: reload = 'no' message = { 'action': 'updateconfig', 'srcfilename': srcfile, 'dstfilename': dstfile, 'reload': reload } for k, v in headers.items(): message[k] = v return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def userEvent(self, event, **headers): """Sends an arbitrary event to the Asterisk Manager Interface.""" message = { 'Action': 'UserEvent', 'userevent': event } for i, j in headers.items(): message[i] = j return self.sendMessage(message) def waitEvent(self, timeout): """Waits for an event to occur After calling this action, Asterisk will send you a Success response as soon as another event is queued by the AMI """ message = { 'action': 'WaitEvent', 'timeout': timeout } return self.collectDeferred(message, 'WaitEventComplete') def dahdiDNDoff(self, channel): """Toggles the DND state on the specified DAHDI channel to off""" message = { 'action': 'DAHDIDNDoff', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiDNDon(self, channel): """Toggles the DND state on the specified DAHDI channel to on""" message = { 'action': 'DAHDIDNDon', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiDialOffhook(self, channel, number): """Dial a number on a DAHDI channel while off-hook""" message = { 'Action': 'DAHDIDialOffhook', 'DAHDIChannel': channel, 'Number': number } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiHangup(self, channel): """Hangs up the specified DAHDI channel""" message = { 'Action': 'DAHDIHangup', 'DAHDIChannel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiRestart(self, channel): """Restarts the DAHDI channels, terminating any calls in progress""" message = { 'Action': 'DAHDIRestart', 'DAHDIChannel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiShowChannels(self): """List all DAHDI channels""" message = { 'action': 'DAHDIShowChannels' } return self.collectDeferred(message, 'DAHDIShowChannelsComplete') def dahdiTransfer(self, channel): """Transfers DAHDI channel""" message = { 'Action': 'DAHDITransfer', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) class AMIFactory(protocol.ReconnectingClientFactory): """A factory for AMI protocols """ protocol = AMIProtocol def __init__(self, username, secret, id=None, plaintext_login=True, on_reconnect=None): self.username = username self.secret = secret self.id = id self.plaintext_login = plaintext_login self.on_reconnect = on_reconnect def login(self, ip='localhost', port=5038, timeout=5, bindAddress=None): """Connect and return protocol instance Connect and return our (singleton) protocol instance with login completed. XXX This is messy, we'd much rather have the factory able to create large numbers of protocols simultaneously """ self.loginDefer = defer.Deferred() reactor.connectTCP(ip, port, self, timeout=timeout, bindAddress=bindAddress) return self.loginDefer def clientConnectionFailed(self, connector, reason): """Connection failed, report to our callers""" self.loginDefer.errback(reason) def clientConnectionLost(self, connector, unused_reason): """Connection lost, re-build the login connection""" log.info('connection lost, reconnecting...') self.retry(connector) self.loginDefer = defer.Deferred() log.info(self.on_reconnect) if self.on_reconnect: self.on_reconnect(self.loginDefer) starpy-1.0.1.0.git.20151124/tools/000077500000000000000000000000001274025253300160145ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/tools/pip-requires000066400000000000000000000000001274025253300203520ustar00rootroot00000000000000starpy-1.0.1.0.git.20151124/tools/test-requires000066400000000000000000000000171274025253300205510ustar00rootroot00000000000000nose pep8==1.1 starpy-1.0.1.0.git.20151124/tox.ini000066400000000000000000000004061274025253300161670ustar00rootroot00000000000000[tox] envlist = py26,py27,pep8 [testenv] deps = -r{toxinidir}/tools/pip-requires -r{toxinidir}/tools/test-requires commands = nosetests {posargs} [testenv:pep8] deps = pep8==1.1 commands = pep8 --repeat --show-source --exclude=.tox,build,doc,examples .