starpy-1.0.1.0.git.20151124/ 0000775 0000000 0000000 00000000000 12740252533 0014654 5 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/.travis.yml 0000664 0000000 0000000 00000000127 12740252533 0016765 0 ustar 00root root 0000000 0000000 language: python
python:
- "2.6"
- "2.7"
- "3.2"
script: python setup.py install
starpy-1.0.1.0.git.20151124/LICENSE 0000664 0000000 0000000 00000003103 12740252533 0015656 0 ustar 00root root 0000000 0000000 Copyright (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.in 0000664 0000000 0000000 00000000127 12740252533 0016412 0 ustar 00root root 0000000 0000000 include *.txt
include LICENSE
include README.rst
recursive-include examples *.txt *.py
starpy-1.0.1.0.git.20151124/README.rst 0000664 0000000 0000000 00000001673 12740252533 0016352 0 ustar 00root root 0000000 0000000 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
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/ 0000775 0000000 0000000 00000000000 12740252533 0015421 5 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/doc/CHANGES.txt 0000664 0000000 0000000 00000001155 12740252533 0017234 0 ustar 00root root 0000000 0000000 CHANGE 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.txt 0000664 0000000 0000000 00000000571 12740252533 0017254 0 ustar 00root root 0000000 0000000 UPGRADE 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.html 0000664 0000000 0000000 00000056643 12740252533 0017434 0 ustar 00root root 0000000 0000000
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:
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:
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:
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:
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:
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:
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:
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:
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:
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 )
StarPy can be downloaded from the project's File
Download area.
1.0.0b1
Provide download link in setup.py to allow easy-install to work
1.0.0a13
Godson's Asterisk 1.4.x interface updates
1.0.0.a12
Fix "recursion" bug in menu's onReadMenu
Fix bug in clientConnectionFailed parameters for AMI connections
1.0.0.a11
Fix bug in fastagi setExtension
Add timeout to manager api
Allow for overriding utilapplication configuration loading (to add new sections, for instance)
1.0.0.a10
Allow for registering a FastAGI handler for None, is used to
provide a default handler for extensions which do not have an explicit
waiter or handler registered
1.0.0.a9
Add multi-element prompt capability, so you can use sound
files, numbers, read alpha and similar operations to define a compound
prompt for a menu.
Note: This change breaks all previously defined menus, you need to
change the "soundFile" property of your menus to be "prompt". You
may (but do not need to) wrap your sound file names in a
menu.AudioPrompt() instance.
Minor bug in onStreamingComplete fixed (variable name shadowed
the module)
1.0.0.a8
Add password-checking menu operation
Add ability to pass an "onSuccess" handler to a menu option; it
is called before returning from selection of that option
Fix bug in AMI handling of multi-line command results that
include ':' characters
Add example showing usage of ami.command(...)
1.0.0.a7
Fix bug introduced in a6
where None could no longer be used to
handle all events in AMI
1.0.0.a6
Fix bug in AMIProtocol.deregisterEvent, would remove all
registrations in all instances
Add ability to register/deregister multiple events at once in
AMIProtocol.registerEvent and deregisterEvent
1.0.0a5
Setup script bug-fix for placement of data files (one directory
level too high)
Minor documentation enhancements
priexhaustion.py example application added (track total number
of open channels)
Bug-fix in AGI getVariable (incorrect/incomplete parsing)
Trivial bug-fix in hellofastagiapp.py (editing problem during
documentation creation)
1.0.0a4
Fixed naming error on setPriority
Added jumpOnError
Fixes for Call Duration Sample to work with newest code
More documentation
1.0.0a3
FastAGI's getOption API change, should actually be useful now
IVR Menu and CollectDigits objects first release
1.0.0a2
Slightly more mature release, a few minor applications have
been built with the package to test out operation, a few bugs have been
fixed.
Call Duration sample application added, note that this requires
BasicProperty
1.0.0a1
Initial release of the
StarPy package, much of the functionality is still untested, but the
coverage of the APIs should be close to complete.
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/ 0000775 0000000 0000000 00000000000 12740252533 0016537 5 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/doc/pydoc/builddocs.py 0000775 0000000 0000000 00000000666 12740252533 0021074 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000035673 12740252533 0020327 0 ustar 00root root 0000000 0000000 """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/ 0000775 0000000 0000000 00000000000 12740252533 0016561 5 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/doc/style/sitestyle.css 0000664 0000000 0000000 00000001421 12740252533 0021316 0 ustar 00root root 0000000 0000000 h1,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/ 0000775 0000000 0000000 00000000000 12740252533 0016472 5 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/examples/__init__.py 0000664 0000000 0000000 00000000075 12740252533 0020605 0 ustar 00root root 0000000 0000000 """Example applications for usage of StarPy with Asterisk"""
starpy-1.0.1.0.git.20151124/examples/amicommand.py 0000664 0000000 0000000 00000002021 12740252533 0021144 0 ustar 00root root 0000000 0000000 #! /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/ 0000775 0000000 0000000 00000000000 12740252533 0020720 5 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/examples/autosurvey/extensions.conf 0000664 0000000 0000000 00000000463 12740252533 0023771 0 ustar 00root root 0000000 0000000 ; 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.py 0000664 0000000 0000000 00000012344 12740252533 0023115 0 ustar 00root root 0000000 0000000 """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.html 0000664 0000000 0000000 00000002505 12740252533 0022717 0 ustar 00root root 0000000 0000000
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:
Enter set of phone numbers or SIP addresses to contact
The owner of the survey is called
Owner can record the options
Owner can view/listen to the options from the web-form
Each user is called and presented with the survey
Results of the survey can be viewed on the web-form
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.py 0000664 0000000 0000000 00000016630 12740252533 0023210 0 ustar 00root root 0000000 0000000 #! /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.conf 0000664 0000000 0000000 00000001125 12740252533 0024141 0 ustar 00root root 0000000 0000000 ; 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.py 0000664 0000000 0000000 00000002106 12740252533 0021560 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000002102 12740252533 0022255 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000001552 12740252533 0022707 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000003104 12740252533 0021327 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000001441 12740252533 0021506 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000002035 12740252533 0022207 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000063316 12740252533 0020021 0 ustar 00root root 0000000 0000000 #
# 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.py 0000664 0000000 0000000 00000005015 12740252533 0020711 0 ustar 00root root 0000000 0000000 #! /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.conf 0000664 0000000 0000000 00000000504 12740252533 0023324 0 ustar 00root root 0000000 0000000 ; 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.py 0000664 0000000 0000000 00000007744 12740252533 0021762 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000004541 12740252533 0022604 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000003745 12740252533 0021672 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000002352 12740252533 0021051 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000002550 12740252533 0021552 0 ustar 00root root 0000000 0000000 #! /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.py 0000664 0000000 0000000 00000020525 12740252533 0022251 0 ustar 00root root 0000000 0000000 #
# 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.txt 0000664 0000000 0000000 00000000010 12740252533 0020127 0 ustar 00root root 0000000 0000000 Twisted
starpy-1.0.1.0.git.20151124/setup.py 0000775 0000000 0000000 00000002541 12740252533 0016373 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 12740252533 0016176 5 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/starpy/__init__.py 0000664 0000000 0000000 00000001126 12740252533 0020307 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000002274 12740252533 0017706 0 ustar 00root root 0000000 0000000 #
# 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.py 0000664 0000000 0000000 00000111501 12740252533 0020165 0 ustar 00root root 0000000 0000000 #
# 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.py 0000664 0000000 0000000 00000115234 12740252533 0020170 0 ustar 00root root 0000000 0000000 #
# 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/ 0000775 0000000 0000000 00000000000 12740252533 0016014 5 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/tools/pip-requires 0000664 0000000 0000000 00000000000 12740252533 0020352 0 ustar 00root root 0000000 0000000 starpy-1.0.1.0.git.20151124/tools/test-requires 0000664 0000000 0000000 00000000017 12740252533 0020551 0 ustar 00root root 0000000 0000000 nose
pep8==1.1
starpy-1.0.1.0.git.20151124/tox.ini 0000664 0000000 0000000 00000000406 12740252533 0016167 0 ustar 00root root 0000000 0000000 [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 .