[Pkg-voip-commits] [starpy] 07/29: Removed starpy source code from package repo
tzafrir at debian.org
tzafrir at debian.org
Fri Aug 14 15:05:11 UTC 2015
This is an automated email from the git hooks/post-receive script.
tzafrir pushed a commit to branch master
in repository starpy.
commit b626b9261eb7a0721c91b36d717b4e2da8086b49
Author: Paul Belanger <paul.belanger at polybeacon.com>
Date: Sat Feb 11 13:36:58 2012 -0500
Removed starpy source code from package repo
Signed-off-by: Paul Belanger <paul.belanger at polybeacon.com>
---
CHANGES.txt | 16 -
LICENSE | 34 --
MANIFEST.in | 25 -
README | 1 -
UPGRADE.txt | 11 -
__init__.py | 13 -
doc/index.html | 707 -------------------------
doc/pydoc/builddocs.py | 27 -
doc/pydoc/pydoc2.py | 463 -----------------
doc/style/sitestyle.css | 54 --
doc/upload.sh | 3 -
error.py | 35 --
examples/__init__.py | 1 -
examples/amicommand.py | 33 --
examples/autosurvey/extensions.conf | 9 -
examples/autosurvey/frontend.py | 167 ------
examples/autosurvey/index.html | 40 --
examples/calldurationcallback.py | 179 -------
examples/calldurationextensions.conf | 20 -
examples/connecttoivr.py | 38 --
examples/connecttoivrapp.py | 36 --
examples/fastagisetvariable.py | 27 -
examples/getvariable.py | 58 ---
examples/hellofastagi.py | 25 -
examples/hellofastagiapp.py | 30 --
examples/menutest.py | 79 ---
examples/menutestextensions.conf | 10 -
examples/priexhaustion.py | 98 ----
examples/priexhaustionbare.py | 62 ---
examples/readingdigits.py | 60 ---
examples/timestamp.py | 45 --
examples/timestampapp.py | 47 --
fastagi.py | 949 ----------------------------------
manager.py | 967 -----------------------------------
menu.py | 630 -----------------------
setup.py | 110 ----
utilapplication.py | 198 -------
37 files changed, 5307 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
deleted file mode 100644
index 7a6d470..0000000
--- a/CHANGES.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-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
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index aa3cb24..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,34 +0,0 @@
-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.
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 3457123..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,25 +0,0 @@
-include MANIFEST.in
-include LICENSE
-include *.py
-include doc/*
-include doc/style/*
-include doc/pydoc/*.py
-include doc/pydoc/*.html
-include examples/*.conf
-
-global-exclude starpy.conf
-global-exclude *CVS*
-global-exclude *Cvs*
-global-exclude *.pyc
-global-exclude *.pyo
-global-exclude *.pdb
-global-exclude *.db
-global-exclude *.max
-global-exclude *.gz
-global-exclude *.zip
-global-exclude *.bat
-global-exclude *.profile
-global-exclude *.directory
-global-exclude *.cvsignore
-global-exclude *.ttf
-global-exclude core.*
diff --git a/README b/README
deleted file mode 100644
index 421a265..0000000
--- a/README
+++ /dev/null
@@ -1 +0,0 @@
-StarPy
diff --git a/UPGRADE.txt b/UPGRADE.txt
deleted file mode 100644
index c4e5680..0000000
--- a/UPGRADE.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-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.
diff --git a/__init__.py b/__init__.py
deleted file mode 100644
index b8b9ac3..0000000
--- a/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""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).
-"""
diff --git a/doc/index.html b/doc/index.html
deleted file mode 100644
index c821fe8..0000000
--- a/doc/index.html
+++ /dev/null
@@ -1,707 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
-<html><head>
-
-
-
-
- <link rel="stylesheet" type="text/css" href="style/sitestyle.css">
-
-
-
-
- <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
-
-
-
-
- <title>StarPy Asterisk Protocols for Twisted</title></head>
-<body>
-
-
-<h1>StarPy
-Asterisk Protocols for Twisted<br>
-
-
-</h1>
-
-
-<p class="introduction">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.</p>
-
-
-<p class="introduction">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.<br>
-
-
-</p>
-
-
-<p class="technical">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.<br>
-
-
-</p>
-
-
-<h2>Installation</h2>
-
-
-<p>StarPy is a pure-Python
-distutils extension. Simply unpack the
-<a href="https://sourceforge.net/project/showfiles.php?group_id=164040">source
-archive</a> to a temporary directory and run:</p>
-
-
-<pre>python setup.py install<br></pre>
-
-
-<p>You
-will need <a href="http://www.python.org/">Python</a> 2.3+ and
-<a href="http://twistedmatrix.com/">Twisted</a> (Core) installed.
-You'll need <a href="http://basicproperty.sourceforge.net/">BasicProperty</a>
-as well. If you want to check out the SVN version instead of a
-released version, use:</p>
-
-
-<pre>svn co https://svn.sourceforge.net/svnroot/starpy/trunk starpy<br></pre>
-
-
-<p>On your PythonPath.</p>
-
-
-<p>The demonstration applications use the <a href="pydoc/starpy.utilapplication.html">utilapplication</a> 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:</p>
-
-
-<pre>[AMI]<br>username=AMIUSERNAME<br>secret=AMIPASSWORD<br>server=127.0.0.1<br>port=5038<br><br>[FastAGI]<br>port=4573<br>interface=127.0.0.1<br>context=survey<br></pre>
-
-
-<p>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)!</p>
-
-
-<h2>Asterisk Manager Interface
-(AMI) Usage</h2>
-
-
-<p><span class="technical"></span></p>
-
-
-<p>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:</p>
-
-
-<pre>from starpy import manager<br>f = manager.AMIFactory(sys.argv[1], sys.argv[2])<br>df = f.login('server',port)</pre>
-
-
-<p>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.</p>
-
-
-<p>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:</p>
-
-
-<pre>[USERNAME]<br>secret=SECRETPASSWORD<br>permit=127.0.0.1<br>read = system,call,log,verbose,command,agent,user<br>write = system,call,log,verbose,command,agent,user</pre>
-
-
-<p>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 <a href="http://www.voip-info.org/wiki/view/Asterisk+manager+API">AMI</a>
-in the voip-info Wiki.<br>
-
-
-</p>
-
-
-<p>The return value for the
-login() deferred is an <a href="pydoc/starpy.manager.html#AMIProtocol">AMIProtocol</a>
-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.</p>
-
-
-<p>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:</p>
-
-
-<pre>self.ami.originate( <br> self.callbackChannel,<br> self.ourContext, id(self), 1,<br> timeout = 15,<br>)<br></pre>
-
-
-<p>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 <a href="pydoc/starpy.utilapplication.html#UtilApplication-waitForCallOn">waitForCallOn</a>
-method to register a one-shot handler if you are using UtilApplication).</p>
-
-
-<p>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.</p>
-
-
-<pre>def onChannelHangup( ami, event ):<br> """Deal with the hangup of an event"""<br> if event['uniqueid'] == self.uniqueChannelId:<br> log.info( """AMI Detected close of our channel: %s""", self.uniqueChannelId )<br> self.stopTime = time.time()<br> # give the user a few seconds to put down the hand-set<br> reactor.callLater( 2, df.callback, event )<br> self.ami.deregisterEvent( 'Hangup', onChannelHangup )<br>self.ami.registerEvent( 'Hangup', onChannelHangup )<br>return df.addCallb [...]
-
-
-<p><span class="technical">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.</span></p>
-
-
-<p>See the
-examples/connecttoivr.py and examples/calldurationcallback.py scripts
-for sample usage of the AMIProtocol</p>
-
-
-<p><span class="technical">Note that StarPy uses floating-point seconds
-for all time values in all interfaces, </span></p>
-
-
-<span style="font-family: monospace;"></span>
-<h2>Fast Asterisk Gateway
-Interface (FastAGI) Usage</h2>
-
-
-<p>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:</p>
-
-
-<pre>from starpy import fastagi<br>f = fastagi.FastAGIFactory(testFunction)<br>reactor.listenTCP( 4573, f, 50, '127.0.0.1')</pre>
-
-
-<p>testFunction in the example above is the
-operation to undertake when the Asterisk Server connects to the FastAGI
-server. It takes a (connected) <a href="pydoc/starpy.fastagi.html#FastAGIProtocol">FastAGIProtocol</a>
-instance as its only argument.</p>
-
-
-<p>This FastAGI protocol has methods available which match those <a href="http://www.voip-info.org/wiki-Asterisk+AGI">AGI functions</a>
-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 <a href="pydoc/starpy.fastagi.html#FastAGIProtocol-execute">execute</a>
-method (known as exec (which is a Python keyword) in the AGI
-documentation) allows you to access <a href="http://www.voip-info.org/wiki/index.php?page=Asterisk+-+documentation+of+application+commands">Asterisk
-Applications</a> as well as AGI methods.</p>
-
-
-<p>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):</p>
-
-
-<pre>exten => 1000,3,AGI(agi://127.0.0.1:4573)</pre>
-
-
-<p>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.<br>
-
-
-</p>
-
-
-<p>See the examples directory for
-examples of FastAGI scripts.</p>
-
-
-<p><span class="technical">Note that StarPy uses floating-point seconds
-for all time values in all interfaces, </span></p>
-
-
-<h3>Sequential Operations</h3>
-
-
-The <a href="pydoc/fastagi.html#InSequence">InSequence</a>
-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:
-<pre>sequence = fastagi.InSequence()<br>sequence.append( agi.setContext, agi.variables['agi_context'] )<br>sequence.append( agi.setExtension, agi.variables['agi_extension'] )<br>sequence.append( agi.setPriority, int(agi.variables['agi_priority'])+difference )<br>sequence.append( agi.finish )<br>return sequence()<br></pre>
-
-
-<p>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.<br>
-
-
-</p>
-
-
-<h3>Menu Objects Usage</h3>
-
-
-<p>The FastAGI interface includes basic support for creating hierarchic
-<a href="pydoc/starpy.menu.html">IVR menus</a>. 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 <a href="pydoc/starpy.menu.html#Menu">Menu</a> using simple single-digit <a href="pydoc/starpy.menu.html#Option">Option</a> instances:</p>
-
-
-<pre>m = menu.Menu(<br> tellInvalid = False, # don't report incorrect selections<br> prompt = 'atlantic',<br> options = [<br> menu.Option( option='0' ),<br> menu.Option( option='#' ),<br> menu.ExitOn( option='*' ),<br> ],<br> maxRepetitions = 5,<br>)<br></pre>
-
-
-<p>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.</p>
-
-
-<p>If a callable option is specified, such as <a href="pydoc/starpy.menu.html#ExitOn">ExitOn</a> or <a href="pydoc/starpy.menu.html#SubMenu">SubMenu</a>, 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:</p>
-
-
-<pre>menu.SubMenu( <br> option='1',<br> menu = menu.Menu(<br> tellInvalid = False, # don't report incorrect selections<br> prompt = ['atlantic',menu.DigitsPrompt(53),menu.DateTimePrompt(time.time())],<br> options = [<br> menu.Option( option='0' ),<br> menu.Option( option='#' ),<br> menu.ExitOn( option='*' ),<br> ],<br> ),<br>),</pre>
-
-
-<p>which can be used as an option within a higher-level menu.</p>
-
-
-<p>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)).</p>
-
-
-<p>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.</p>
-
-
-<p>The Menu module includes a <a href="pydoc/starpy.menu.html#CollectDigits">CollectDigits</a> class
-which may be used either as a top-level Menu or as a SubMenu-wrapped
-option in a higher-level menu:</p>
-
-
-<pre>menu.SubMenu(<br> option='2',<br> menu = menu.CollectDigits(<br> soundFile = 'extension',<br> maxDigits = 5,<br> minDigits = 3,<br> ),<br>)<br></pre>
-
-
-<p>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.</p>
-
-
-<h3>Signalling Errors from FastAGI<br>
-
-
-</h3>
-
-
-<p>The FastAGIProtocol has a method <a href="pydoc/fastagi.html#FastAGIProtocol-jumpOnError">jumpOnError</a>
-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.<br>
-
-
-</p>
-
-
-<pre>df.addErrback( agi.jumpOnError, 100 )<br></pre>
-
-
-<p>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:</p>
-
-
-<pre>df.addErrback( agi.jumpOnError, 50, forErrors=error.OnUnknownUser )<br></pre>
-
-
-<h2>Secondary Services</h2>
-
-
-<p>The <a href="pydoc/starpy.utilapplication.html">utilapplication</a>
-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.</p>
-
-
-<pre># map incoming calls to extension 's' to the given method onS<br>APPLICATION.handleCallsFor( 's', someObject.onS )</pre>
-
-
-<p>UtilApplication's agiSpecifier and amiSpecifier property point to
-automatically generated <a href="pydoc/starpy.utilapplication.html#AGISpecifier">AGISpecifier</a>
-and <a href="pydoc/starpy.utilapplication.html#AMISpecifier">AMISpecifier</a>
-instances whose parameters are loaded from configuration files.
-The specifier instances provide methods for starting up instances
-configured by the specifier:</p>
-
-
-<pre># tell the application to run a FastAGI server which dispatches<br># to handlers registered with handleCallsFor (as above)<br>APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall )<br><br># tell the application to log into the configured AMI server<br># to allow for further management operations<br>df = APPLICATION.amiSpecifier.login( <br>).addCallback( self.onAMIConnect )<br></pre>
-
-
-<h3>Simple FastAGI Application Example<br>
-
-
-</h3>
-
-
-<p>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'):</p>
-
-
-<pre>#! /usr/bin/env python<br>"""FastAGI server using starpy and the utility application framework<br><br>This is basically identical to hellofastagi, save that it uses the application<br>framework to allow for configuration-file-based setup of the AGI service.<br>"""<br>from twisted.internet import reactor<br>from starpy import fastagi, utilapplication<br>import logging, time<br><br>log = logging.getLogger( 'hellofastagi' )<br><br>def testFunction( agi ):<br> """Demonstrate simplistic [...]
-
-
-<h2>Changes</h2>
-
-
-<ul>
-
-
-</ul>
-
-
-<p>StarPy can be downloaded from the project's <a href="https://sourceforge.net/project/showfiles.php?group_id=164040">File
-Download</a> area.</p>
-
-
-<ul>
-
-
- <li>1.0.0b1</li><ul><li>Provide download link in setup.py to allow easy-install to work</li></ul><li>1.0.0a13</li><ul><li>Godson's Asterisk 1.4.x interface updates</li></ul><li>1.0.0.a12</li>
- <ul>
- <li>Fix "recursion" bug in menu's onReadMenu</li>
- <li>Fix bug in clientConnectionFailed parameters for AMI connections</li>
- </ul>
- <li>1.0.0.a11</li>
-
-
- <ul>
-
- <li>Fix bug in fastagi setExtension</li>
-
- <li>Add timeout to manager api</li>
-
- <li>Allow for overriding utilapplication configuration loading (to add new sections, for instance)</li>
-
-
- </ul>
-
- <li>1.0.0.a10</li>
-
-
-
-
- <ul>
-
-
- <li>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</li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0.a9</li>
-
-
-
-
- <ul>
-
-
- <li>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.<br>
-
-
- <br>
-
-
-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.</li>
-
-
- <li>Minor bug in onStreamingComplete fixed (variable name shadowed
-the module)</li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0.a8</li>
-
-
-
-
- <ul>
-
-
- <li>Add password-checking menu operation</li>
-
-
- <li>Add ability to pass an "onSuccess" handler to a menu option; it
-is called before returning from selection of that option</li>
-
-
- <li>Fix bug in AMI handling of multi-line command results that
-include ':' characters</li>
-
-
- <li>Add example showing usage of ami.command(...)</li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0.a7</li>
-
-
-
-
- <ul>
-
-
- <li>Fix bug introduced in a6
-where None could no longer be used to
-handle all events in AMI</li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0.a6</li>
-
-
-
-
- <ul>
-
-
- <li>Fix bug in AMIProtocol.deregisterEvent, would remove all
-registrations in all instances<br>
-
-
- </li>
-
-
- <li>Add ability to register/deregister multiple events at once in
-AMIProtocol.registerEvent and deregisterEvent</li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0a5</li>
-
-
-
-
- <ul>
-
-
- <li>Setup script bug-fix for placement of data files (one directory
-level too high)</li>
-
-
- <li>Minor documentation enhancements<br>
-
-
- </li>
-
-
- <li>priexhaustion.py example application added (track total number
-of open channels)</li>
-
-
- <li>Bug-fix in AGI getVariable (incorrect/incomplete parsing)</li>
-
-
- <li>Trivial bug-fix in hellofastagiapp.py (editing problem during
-documentation creation)</li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0a4</li>
-
-
-
-
- <ul>
-
-
- <li>Fixed naming error on setPriority</li>
-
-
- <li>Added jumpOnError</li>
-
-
- <li>Fixes for Call Duration Sample to work with newest code</li>
-
-
- <li>More documentation</li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0a3</li>
-
-
-
-
- <ul>
-
-
- <li>FastAGI's getOption API change, should actually be useful now</li>
-
-
- <li>IVR Menu and CollectDigits objects first release</li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0a2</li>
-
-
-
-
- <ul>
-
-
- <li>Slightly more mature release, a few minor applications have
-been built with the package to test out operation, a few bugs have been
-fixed.</li>
-
-
- <li>Call Duration sample application added, note that this requires
- <a href="http://basicproperty.sourceforge.net/">BasicProperty</a><br>
-
-
- </li>
-
-
-
-
- </ul>
-
-
- <li>1.0.0a1</li>
-
-
-
-
- <ul>
-
-
- <li>Initial release of the
-StarPy package, much of the functionality is still untested, but the
-coverage of the APIs should be close to complete.</li>
-
-
-
-
- </ul>
-
-
-</ul>
-
-
-<h2>License</h2>
-
-
-<p>StarPy is licensed under
-extremely liberal terms.<br>
-
-
-</p>
-
-
-<pre>Copyright (c) 2006, Michael C. Fletcher and Contributors<br>All rights reserved.<br><br>Redistribution and use in source and binary forms, with or without<br>modification, are permitted provided that the following conditions<br>are met:<br><br> Redistributions of source code must retain the above copyright<br> notice, this list of conditions and the following disclaimer.<br><br> Redistributions in binary form must reproduce the above<br> copyright notice, this list of conditions and [...]
-
-
-<p class="footer"><a href="http://starpy.sourceforge.net/">StarPy</a>
-is a <a href="http://sourceforge.net/"> <img alt="SourceForge.net Logo" style="border: 0px solid ; width: 88px; height: 31px;" src="http://sourceforge.net/sflogo.php?group_id=164040&type=1" align="middle"></a> Open-Source Project</p>
-
-
-</body></html>
\ No newline at end of file
diff --git a/doc/pydoc/builddocs.py b/doc/pydoc/builddocs.py
deleted file mode 100755
index 080f7c6..0000000
--- a/doc/pydoc/builddocs.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""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 ()
-
diff --git a/doc/pydoc/pydoc2.py b/doc/pydoc/pydoc2.py
deleted file mode 100644
index 8fc4d33..0000000
--- a/doc/pydoc/pydoc2.py
+++ /dev/null
@@ -1,463 +0,0 @@
-"""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(
- '<a href="%s.html"><font color="#ffffff">%s</font></a>' %
- (join(parts[:i+1], '.'), parts[i]))
- linkedname = join(links + parts[-1:], '.')
- head = '<big><big><strong>%s</strong></big></big>' % linkedname
- try:
- path = inspect.getabsfile(object)
- url = path
- if sys.platform == 'win32':
- import nturl2path
- url = nturl2path.pathname2url(path)
- filelink = '<a href="file:%s">%s</a>' % (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', '<a href=".">index</a><br>' + 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 '<tt>%s</tt>' % doc
- result = result + '<p>%s</p>\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, '<br>\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 '<a href="%s.html#%s">%s</a>' % (
- 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, '<br>')
- 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 ()
-
diff --git a/doc/style/sitestyle.css b/doc/style/sitestyle.css
deleted file mode 100644
index 2db80f2..0000000
--- a/doc/style/sitestyle.css
+++ /dev/null
@@ -1,54 +0,0 @@
-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;
-}
diff --git a/doc/upload.sh b/doc/upload.sh
deleted file mode 100755
index 039c8e7..0000000
--- a/doc/upload.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /bin/sh
-rsync -r -R -L -v -l -C -p --exclude=*.py --exclude=*.sh --exclude=digikam.xml --exclude=Thumbs.db --exclude=thumbs.db --exclude=*.in --rsh=ssh ./* mcfletch at shell.sourceforge.net:~/starpy/doc/
-ssh mcfletch at shell.sourceforge.net "/usr/local/bin/sfgrp starpy < ~/mvstarpysite.sh"
diff --git a/error.py b/error.py
deleted file mode 100644
index f3dfa3c..0000000
--- a/error.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#
-# StarPy -- Asterisk Protocols for Twisted
-#
-# Copyright (c) 2006, Michael C. Fletcher
-#
-# Michael C. Fletcher <mcfletch at vrplumber.com>
-#
-# 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 exits (i.e. not a choice) from a menu"""
-
-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?"""
diff --git a/examples/__init__.py b/examples/__init__.py
deleted file mode 100644
index d11c21c..0000000
--- a/examples/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Example applications for usage of StarPy with Asterisk"""
diff --git a/examples/amicommand.py b/examples/amicommand.py
deleted file mode 100644
index 232eb52..0000000
--- a/examples/amicommand.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#! /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, utilapplication, 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()
diff --git a/examples/autosurvey/extensions.conf b/examples/autosurvey/extensions.conf
deleted file mode 100644
index 4eb01dd..0000000
--- a/examples/autosurvey/extensions.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-; 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()
diff --git a/examples/autosurvey/frontend.py b/examples/autosurvey/frontend.py
deleted file mode 100644
index da50378..0000000
--- a/examples/autosurvey/frontend.py
+++ /dev/null
@@ -1,167 +0,0 @@
-"""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()
diff --git a/examples/autosurvey/index.html b/examples/autosurvey/index.html
deleted file mode 100644
index 9708cc8..0000000
--- a/examples/autosurvey/index.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
-<html>
-<head>
- <meta content="text/html; charset=ISO-8859-1"
- http-equiv="content-type">
- <title>Autosurvey Demo Application</title>
-</head>
-<body>
-<h1>Autosurvey Demo Application</h1>
-<p>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.</p>
-<p>Features:</p>
-<ul>
- <li>Enter set of phone numbers or SIP addresses to contact</li>
- <li>The owner of the survey is called</li>
- <ul>
- <li>Owner can record the options</li>
- <li>Owner can view/listen to the options from the web-form</li>
- </ul>
- <li>Each user is called and presented with the survey</li>
- <li>Results of the survey can be viewed on the web-form<br>
- </li>
-</ul>
-<h2>Setup<br>
-</h2>
-<p>Enter your phone number here:</p>
-<form enctype="multipart/form-data" method="post" action="/record"
- name="record">
-<p><input name="ownerName" value="mike"><input
- name="recordOptions" value="Record Survey" type="submit"></p>
-</form>
-<p>The survey server will call you to record the survey options...</p>
-<h3>Introduction</h3>
-<h3>Options</h3>
-<p>Enter the participant's phone numbers here:</p>
-<p><textarea cols="30" rows="4" name="participants"></textarea></p>
-<h2>Results</h2>
-</body>
-</html>
diff --git a/examples/calldurationcallback.py b/examples/calldurationcallback.py
deleted file mode 100644
index e9c8292..0000000
--- a/examples/calldurationcallback.py
+++ /dev/null
@@ -1,179 +0,0 @@
-#! /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, utilapplication, 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()
diff --git a/examples/calldurationextensions.conf b/examples/calldurationextensions.conf
deleted file mode 100644
index 7db4894..0000000
--- a/examples/calldurationextensions.conf
+++ /dev/null
@@ -1,20 +0,0 @@
-; 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 at 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()
diff --git a/examples/connecttoivr.py b/examples/connecttoivr.py
deleted file mode 100644
index 24dcf63..0000000
--- a/examples/connecttoivr.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""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 at 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()
diff --git a/examples/connecttoivrapp.py b/examples/connecttoivrapp.py
deleted file mode 100644
index d398b20..0000000
--- a/examples/connecttoivrapp.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""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, utilapplication
-from twisted.internet import reactor
-import sys, logging
-APPLICATION = utilapplication.UtilApplication()
-
-def main( channel = 'sip/4167290048 at 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()
diff --git a/examples/fastagisetvariable.py b/examples/fastagisetvariable.py
deleted file mode 100644
index 3ad2041..0000000
--- a/examples/fastagisetvariable.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#! /usr/bin/env python
-"""Try to set a FastAGI variable"""
-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' )
- 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()
diff --git a/examples/getvariable.py b/examples/getvariable.py
deleted file mode 100644
index c8d5498..0000000
--- a/examples/getvariable.py
+++ /dev/null
@@ -1,58 +0,0 @@
-#! /usr/bin/env python
-"""Demonstrate usage of getVariable on the agi interface...
-"""
-from twisted.internet import reactor
-from starpy import fastagi, 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()
diff --git a/examples/hellofastagi.py b/examples/hellofastagi.py
deleted file mode 100644
index 1434dad..0000000
--- a/examples/hellofastagi.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#! /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()
diff --git a/examples/hellofastagiapp.py b/examples/hellofastagiapp.py
deleted file mode 100644
index 9ec9fb4..0000000
--- a/examples/hellofastagiapp.py
+++ /dev/null
@@ -1,30 +0,0 @@
-#! /usr/bin/env python
-"""FastAGI server using starpy and the utility application framework
-
-This is basically identical to hellofastagi, save that it uses the application
-framework to allow for configuration-file-based setup of the AGI service.
-"""
-from twisted.internet import reactor
-from starpy import fastagi, utilapplication
-import logging, time
-
-log = logging.getLogger( 'hellofastagi' )
-
-def testFunction( agi ):
- """Demonstrate simplistic use of the AGI interface with sequence of actions"""
- log.debug( 'testFunction' )
- sequence = fastagi.InSequence()
- sequence.append( agi.sayDateTime, time.time() )
- sequence.append( agi.finish )
- def onFailure( reason ):
- log.error( "Failure: %s", reason.getTraceback())
- agi.finish()
- return sequence().addErrback( onFailure )
-
-if __name__ == "__main__":
- logging.basicConfig()
- fastagi.log.setLevel( logging.DEBUG )
- APPLICATION = utilapplication.UtilApplication()
- APPLICATION.handleCallsFor( 's', testFunction )
- APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall )
- reactor.run()
diff --git a/examples/menutest.py b/examples/menutest.py
deleted file mode 100644
index 18b5f27..0000000
--- a/examples/menutest.py
+++ /dev/null
@@ -1,79 +0,0 @@
-#! /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, utilapplication, menu, error
-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()
diff --git a/examples/menutestextensions.conf b/examples/menutestextensions.conf
deleted file mode 100644
index 7d218db..0000000
--- a/examples/menutestextensions.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-; 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()
diff --git a/examples/priexhaustion.py b/examples/priexhaustion.py
deleted file mode 100644
index 09d6d5f..0000000
--- a/examples/priexhaustion.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#! /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, utilapplication, 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()
diff --git a/examples/priexhaustionbare.py b/examples/priexhaustionbare.py
deleted file mode 100644
index 21875c6..0000000
--- a/examples/priexhaustionbare.py
+++ /dev/null
@@ -1,62 +0,0 @@
-#! /usr/bin/env python
-from twisted.application import service, internet
-from twisted.internet import reactor, defer
-from starpy import manager, fastagi, utilapplication, 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()
diff --git a/examples/readingdigits.py b/examples/readingdigits.py
deleted file mode 100644
index 093f1be..0000000
--- a/examples/readingdigits.py
+++ /dev/null
@@ -1,60 +0,0 @@
-#! /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()
diff --git a/examples/timestamp.py b/examples/timestamp.py
deleted file mode 100644
index e66e832..0000000
--- a/examples/timestamp.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#! /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()
diff --git a/examples/timestampapp.py b/examples/timestampapp.py
deleted file mode 100644
index b766ead..0000000
--- a/examples/timestampapp.py
+++ /dev/null
@@ -1,47 +0,0 @@
-#! /usr/bin/env python
-"""Provide a trivial date-and-time service"""
-from twisted.internet import reactor
-from starpy import fastagi, 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()
diff --git a/fastagi.py b/fastagi.py
deleted file mode 100644
index 731bee1..0000000
--- a/fastagi.py
+++ /dev/null
@@ -1,949 +0,0 @@
-#
-# StarPy -- Asterisk Protocols for Twisted
-#
-# Copyright (c) 2006, Michael C. Fletcher
-#
-# Michael C. Fletcher <mcfletch at vrplumber.com>
-#
-# 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 the manager"""
- 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, 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, 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 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)
-
- 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(
- self.checkFailure, failure='-1', # docs don't show a failure case, actually
- ).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) or drop if neither 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(
- self.checkFailure, failure='0', # planned eventual failure case (not capable)
- ).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 we are 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
diff --git a/manager.py b/manager.py
deleted file mode 100644
index 6a3adb8..0000000
--- a/manager.py
+++ /dev/null
@@ -1,967 +0,0 @@
-#
-# StarPy -- Asterisk Protocols for Twisted
-#
-# Copyright (c) 2006, Michael C. Fletcher
-#
-# Michael C. Fletcher <mcfletch at vrplumber.com>
-#
-# 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, logging
-from starpy import error
-
-log = logging.getLogger('AMI')
-
-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')
- df = self.login()
-
- 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 message.has_key('actionid'):
- 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 message.has_key('event'):
- 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 = defer.Deferred()
- actionid = self.sendMessage(message, df.callback)
- df.addCallbacks(
- self.cleanup, self.cleanup,
- callbackArgs=(actionid,), errbackArgs=(actionid,)
- )
- return df
-
- 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
- """
- message = dict([(k.lower(),v) for (k,v) in message.items()])
- if not message.has_key('actionid'):
- 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('')
- 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['event'] == stopEvent:
- 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 a 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 console command, return deferred result for line 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):
- return message[' ']
-
- return df.addCallback(onResult)
-
- 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']
- return df.callback(value)
-
- message = {
- 'Action' : 'DBGet',
- 'family' : family,
- 'key' : key
- }
- self.sendDeferred(message).addCallback(self.errorUnlessResponse)
- 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):
- """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"""
-
- def extractVariable(message):
- """When message comes in, extract the variable from it"""
- if message.has_key(variable.lower()):
- value = message[variable.lower()]
- elif message.has_key('value'):
- value = message['value']
- else:
- raise error.AMICommandFailure(message)
- if value == '(null)':
- value = None
- return value
-
- message = {
- 'action' : 'getvar',
- 'channel' : channel,
- 'variable' : variable
- }
- 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 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
- ):
- """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
- """
- variable = '|'.join(["%s=%s" % (x[0], x[1]) for x in variable.items()])
- message = dict([(k,v) for (k,v) in {
- 'action' : 'originate',
- 'channel' : channel,
- 'context' : context,
- 'exten' : exten,
- 'priority' : priority,
- 'timeout' : timeout,
- 'callerid' : callerid,
- 'account' : account,
- 'application' : application,
- 'data' : data,
- 'variable' : variable,
- 'async' : str(async)
- }.items() if v is not None])
- if message.has_key('timeout'):
- message['timeout'] = message['timeout'] * 1000
- 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):
- """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
- }
- return self.sendDeferred(message).addCallback(self.errorUnlessResponse)
-
- def queuePause(self, queue, interface, paused = True):
- if paused in (True,'true',1):
- paused = 'true'
- else:
- paused = 'false'
- message = {
- 'action' : 'queuepause',
- 'queue' : queue,
- 'interface' : interface,
- 'paused' : paused
- }
- 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):
- """Retrieve information about active queues via multiple events"""
- message = {
- 'action' : 'queuestatus'
- }
- 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"""
- message = {
- 'action' : 'setvar',
- 'channel' : channel,
- 'variable' : variable,
- 'value' : value
- }
- 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 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 do not disturb state on the specified DAHDI channel to off"""
- messge = {
- 'action' : 'DAHDIDNDoff',
- 'channel' : channel
- }
- return self.sendDeferred(message).addCallback(self.errorUnlessResponse)
-
- def dahdiDNDon(self, channel):
- """Toggles the do not disturb state on the specified DAHDI channel to on"""
- messge = {
- 'action' : 'DAHDIDNDon',
- 'channel' : channel
- }
- return self.sendDeferred(message).addCallback(self.errorUnlessResponse)
-
- def dahdiDialOffhook(self, channel, number):
- """Dials the specified number on the DAHDI channel while the phone is 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):
- """Completly 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.ClientFactory):
- """A factory for AMI protocols
- """
- protocol = AMIProtocol
-
- def __init__(self, username, secret, id=None):
- self.username = username
- self.secret = secret
- self.id = id
-
- def login(self, ip='localhost', port=5038, timeout=5):
- """Connect, returning 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)
- return self.loginDefer
-
- def clientConnectionFailed(self, connector, reason):
- """Connection failed, report to our callers"""
- self.loginDefer.errback(reason)
-
diff --git a/menu.py b/menu.py
deleted file mode 100644
index 2004539..0000000
--- a/menu.py
+++ /dev/null
@@ -1,630 +0,0 @@
-#
-# StarPy -- Asterisk Protocols for Twisted
-#
-# Copyright (c) 2006, Michael C. Fletcher
-#
-# Michael C. Fletcher <mcfletch at vrplumber.com>
-#
-# 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, utilapplication, error
-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 )
diff --git a/setup.py b/setup.py
deleted file mode 100755
index de51866..0000000
--- a/setup.py
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/usr/bin/env python
-#
-# StarPy -- Asterisk Protocols for Twisted
-#
-# Copyright (c) 2006, Michael C. Fletcher
-#
-# Michael C. Fletcher <mcfletch at vrplumber.com>
-#
-# 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.
-
-"""Installs StarPy using distutils
-
-Run:
- python setup.py install
-to install the package from the source archive.
-"""
-
-if __name__ == "__main__":
- import sys,os, string
- from distutils.sysconfig import *
- from distutils.core import setup
-
- ##############
- ## Following is from Pete Shinners,
- ## apparently it will work around the reported bug on
- ## some unix machines where the data files are copied
- ## to weird locations if the user's configuration options
- ## were entered during the wrong phase of the moon :) .
- from distutils.command.install_data import install_data
- class smart_install_data(install_data):
- def run(self):
- #need to change self.install_dir to the library dir
- install_cmd = self.get_finalized_command('install')
- self.install_dir = getattr(install_cmd, 'install_lib')
- # should create the directory if it doesn't exist!!!
- return install_data.run(self)
- ##############
- def npFilesFor( dirname ):
- """Return all non-python-file filenames in dir"""
- result = []
- allResults = []
- for name in os.listdir(dirname):
- path = os.path.join( dirname, name )
- if os.path.isfile( path) and os.path.splitext( name )[1] not in ('.py','.pyc','.pyo') and name!='starpy.conf':
- result.append( path )
- elif os.path.isdir( path ) and name.lower() !='cvs':
- allResults.extend( npFilesFor(path))
- if result:
- allResults.append( (dirname, result))
- return allResults
- dataFiles = npFilesFor( 'doc') + npFilesFor( 'examples') + [('.',('LICENSE',))]
- dataFiles = [
- (os.path.join('starpy',directory), files)
- for (directory,files) in dataFiles
- ]
-
- from sys import hexversion
- if hexversion >= 0x2030000:
- # work around distutils complaints under Python 2.2.x
- extraArguments = {
- 'classifiers': [
- """License :: OSI Approved :: BSD License""",
- """Programming Language :: Python""",
- """Topic :: Software Development :: Libraries :: Python Modules""",
- """Intended Audience :: Developers""",
- ],
- 'keywords': 'asterisk,fastagi,twisted,protocol,manager,ami',
- 'long_description' : """Twisted Protocols for interaction with Asterisk PBX
-
-Provides Asterisk AMI and Asterisk FastAGI protocols under Twisted,
-allowing for fairly extensive customisation of Asterisk operations
-from a Twisted process.""",
- 'platforms': ['Any'],
- }
- else:
- extraArguments = {
- }
- ### Now the actual set up call
- setup (
- name = "starpy",
- version = '1.0.0b1',
- url = "http://starpy.sourceforge.net",
- download_url = "http://sourceforge.net/project/showfiles.php?group_id=164040",
- description = "Twisted Protocols for interaction with the Asterisk PBX",
- author = "Mike C. Fletcher",
- author_email = "mcfletch at vrplumber.com",
- license = "BSD",
-
- package_dir = {
- 'starpy':'.',
- },
- packages = [
- 'starpy',
- 'starpy.examples',
- ],
- options = {
- 'sdist':{'force_manifest':1,'formats':['gztar','zip'],},
- },
- data_files = dataFiles,
- cmdclass = {'install_data':smart_install_data},
- **extraArguments
- )
-
diff --git a/utilapplication.py b/utilapplication.py
deleted file mode 100644
index 0e51901..0000000
--- a/utilapplication.py
+++ /dev/null
@@ -1,198 +0,0 @@
-#
-# StarPy -- Asterisk Protocols for Twisted
-#
-# Copyright (c) 2006, Michael C. Fletcher
-#
-# Michael C. Fletcher <mcfletch at vrplumber.com>
-#
-# 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)
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-voip/starpy.git
More information about the Pkg-voip-commits
mailing list