pax_global_header00006660000000000000000000000064117167515300014520gustar00rootroot0000000000000052 comment=a6ec63711024e59709396f2492f0a9ed51f998df starpy-1.0.1/000077500000000000000000000000001171675153000130415ustar00rootroot00000000000000starpy-1.0.1/CHANGES.txt000066400000000000000000000011551171675153000146540ustar00rootroot00000000000000CHANGE notes for starpy python module ====================================== - References to zaptel functions have been converted into their DAHDI equivalents. For most of the functions, the first three letters of the old function names "zap", have been removed and replaced with "dahdi". Old Zaptel Func. | New Zaptel Func. ====================================== zapDNDon | dahdiDNDon zapDNDoff | dahdiDNDoff zapDialOffHook | dahdiDialOffHook zapHangup | dahdiHangup zapshowchannels | dahdiShowChannels zaptransfers | dahdiTransfers starpy-1.0.1/ChangeLog000066400000000000000000000563341171675153000146260ustar00rootroot00000000000000commit 26af20f76f555f625fceddd95625479a133aa86e Author: Paul Belanger Date: Wed Feb 15 10:45:42 2012 -0500 Don't build and install examples Signed-off-by: Paul Belanger commit 158609b5d8d10021fa11f6d9bddd90ba093158c3 Author: Paul Belanger Date: Wed Feb 15 10:13:31 2012 -0500 Update ChangeLog Signed-off-by: Paul Belanger commit 56ae6b982e0086ca3c0d7e37265122094476a91c Author: Paul Belanger Date: Wed Feb 15 09:53:51 2012 -0500 Prepare for 1.0.1 release Signed-off-by: Paul Belanger commit 162c2543ba66095be404bf30269ed10edcb9bf01 Merge: e7d94e0 eebdf36 Author: Paul Belanger Date: Tue Feb 14 11:46:30 2012 -0800 Merge pull request #8 from russellb/master Move some stuff into examples, pep8 fixes Signed-off-by: Paul Belanger commit eebdf36c9e5c829d44aff5e9850d4eb362f0b879 Author: Russell Bryant Date: Mon Feb 13 21:42:08 2012 -0500 Make core lib pep8 clean commit 520f9e64b57c24b720754c8150a629e0c71f0542 Author: Russell Bryant Date: Mon Feb 13 21:03:30 2012 -0500 Move menu and utilapplication into examples. commit e7d94e0ae936c1b9db90304a94f0f858fa33a723 Author: Paul Belanger Date: Mon Feb 13 20:31:01 2012 -0500 Also drop examples from being installed Signed-off-by: Paul Belanger commit 9f02fc5d03c60d4d54a40a796388a88a7418925d Author: Paul Belanger Date: Mon Feb 13 20:29:55 2012 -0500 Don't install LICENSE file Signed-off-by: Paul Belanger commit de8e875ce399e8a123fcf8056f0d7154824829f2 Author: Paul Belanger Date: Fri Feb 10 22:07:20 2012 -0500 Create .gitattributes file Signed-off-by: Paul Belanger commit 1058aabe4bb86ded7e1f16f11ac82f842e1285ab Author: Paul Belanger Date: Fri Feb 10 21:42:44 2012 -0500 Create .gitignore file Signed-off-by: Paul Belanger commit b2828107b1d795856ea373ad0d75e442b131db98 Author: Paul Belanger Date: Fri Feb 10 21:39:02 2012 -0500 Prepare for 1.0.0 release Signed-off-by: Paul Belanger commit 7c809b28e045337f9b2cf4984693309c0e911cd4 Merge: 1c3c4e7 1544697 Author: Paul Belanger Date: Tue Nov 29 13:53:40 2011 -0500 Merge branch 'master' of github.com:asterisk-org/starpy commit 1544697c21d9c7d059e11b066be812dcd9147bb0 Merge: ca30d9c 4be1f77 Author: Paul Belanger Date: Tue Nov 29 10:48:27 2011 -0800 Merge pull request #7 from ZooKeeper/master Here's a small bit that caused my server to die several times Signed-off-by: Paul Belanger commit 1c3c4e7a443ad7d93f6e4c85dca36fc39258bee4 Author: Paul Belanger Date: Sat Nov 26 12:21:22 2011 -0500 Coding guidelines Signed-off-by: Paul Belanger commit 32b6d1f33a3bdce49d4f126c49930f484d90005a Author: Paul Belanger Date: Sat Nov 26 11:54:25 2011 -0500 Remove sourceforge references Signed-off-by: Paul Belanger commit ca30d9c46358f53c1c18392147e3f327e57a59a9 Author: Paul Belanger Date: Sat Nov 26 11:50:24 2011 -0500 manager: use ',' as a variable separator Signed-off-by: Tzafrir Cohen Signed-off-by: Paul Belanger commit 4be1f775cbb5b0ace3c7ff10174557e635fbb522 Author: Pavel Skvazh Date: Wed Oct 26 16:32:44 2011 +0400 There often is no 'event' element in the dict commit 1d29651c0ee212b285f1571b65f38dbaeb46ffd5 Author: Paul Belanger Date: Thu Oct 20 16:13:02 2011 -0400 Fix typo in previous pull request Signed-off-by: Paul Belanger commit 52c8adaad3925fba3d0c9e6cc41ee015230568b8 Merge: 02ee9f6 c6e1233 Author: Paul Belanger Date: Thu Oct 20 12:47:24 2011 -0700 Merge pull request #6 from ZooKeeper/master Fails on certain events Signed-off-by: Paul Belanger commit c6e123339aa0b941450a0300a7989f917c3adcb3 Author: Pavel Skvazh Date: Thu Oct 20 23:02:12 2011 +0400 Check if the message is a dict commit 02ee9f60a9fa7e2ca7b212cac6b21b2fa2be3d1d Merge: 875954e caae7b6 Author: Paul Belanger Date: Fri Sep 9 13:39:22 2011 -0400 Merge remote branch 'sustav/master' commit 875954e1682132d835b435702a79889f2381aba7 Author: Paul Belanger Date: Fri Sep 9 13:21:46 2011 -0400 Log message to DEBUG level Signed-off-by: Paul Belanger commit caae7b6bedaea54d9b6e216f1cd92e90a8adb196 Author: Vladislav Povorozniuc Date: Mon Aug 22 11:10:06 2011 -0500 Remove formatting unrelated to the main purpose of the patch. commit 387fbff929be112473237c2ec887ffc67707a20f Author: Vladislav Povorozniuc Date: Fri Aug 19 14:02:43 2011 -0500 Added dbDel and dbDelTree manager actions to AMIProtocol class. commit 2cc59e01a1ff300842dc3a3f1e9fc640f2975898 Author: Paul Belanger Date: Wed Jun 29 10:11:09 2011 -0400 s/license.txt/LICENSE/ commit 58bf1074334f69e42df108151e2c6fa62d60a066 Merge: a9d0bd5 b064d93 Author: Paul Belanger Date: Tue Jun 28 11:40:55 2011 -0700 Merge pull request #4 from pabelanger/master Add LICENSE information to headers commit b064d931850e7d1a9195df37e554584e5285847c Author: Paul Belanger Date: Tue Jun 28 14:18:29 2011 -0400 Remove white space. Signed-off-by: Paul Belanger commit 0307c3b7d4d6fc7f53a104bca87fedaef7650c93 Author: Paul Belanger Date: Tue Jun 28 14:14:13 2011 -0400 s/.org/.com/ Signed-off-by: Paul Belanger commit ec1361e6157b5bcf88276cd5ee910217334cbf62 Author: Paul Belanger Date: Tue Jun 28 14:10:15 2011 -0400 Update headers to include LICENSE information. Signed-off-by: Paul Belanger commit a9d0bd5cc0653369ddba2144bb184be8aa714e07 Author: Paul Belanger Date: Tue Jun 28 13:30:10 2011 -0400 Create basic README file. Signed-off-by: Paul Belanger commit 87e74c5b72bf4af22f84cd5235513fdae3ff872e Merge: 5dbd813 0c91a97 Author: Paul Belanger Date: Thu Jun 16 06:38:30 2011 -0700 Merge pull request #3 from brettbryant/master Zaptel to DAHDI conversions for manager.py commit 0c91a9752b3182c220643a7afce63a62bbe36ac7 Author: Brett Bryant Date: Fri Jun 3 16:24:52 2011 -0400 Adding UPGRADE.txt and CHANGES.txt to describe the move from zaptel to dahdi functions in the manager. commit 99f5a7fe5ee106e2607eccf363225b5ee8c34761 Author: Brett Bryant Date: Fri Jun 3 13:19:21 2011 -0400 Changed old zaptel manager actions to their new DAHDI equivalents. commit 5dbd813c1ed5cf2c36054204451e6c6d7cfb6970 Author: Paul Belanger Date: Thu May 19 17:37:26 2011 -0400 Fix typo Signed-off-by: Paul Belanger commit 42666566421fec24ec46c47b2896e5bffbe3f8ab Author: Paul Belanger Date: Thu May 19 17:06:32 2011 -0400 Additional code formatting Signed-off-by: Paul Belanger commit fdda406ab20468040b3d68d59576950d41dd8fb2 Author: Paul Belanger Date: Fri May 6 19:56:48 2011 -0400 Retab and remove red blobs. Signed-off-by: Paul Belanger commit c6d86dbfbb7417efce0805b02e9684cb1c37deeb Author: pabelanger Date: Fri May 6 19:49:14 2011 -0400 Formatting changes; :retab to remove red blobs. Signed-off-by: pabelanger commit 80c88ade1dc928fd2608b8fa2d5ecc81548c4dc0 Author: Russell Bryant Date: Fri May 6 17:34:38 2011 -0500 Add a blank line (testing irc hook). Signed-off-by: Russell Bryant commit fb0d646a8d6dfa8fd797acd33b999eef42246def Merge: 12f426a b60603e Author: Russell Bryant Date: Wed May 4 14:53:02 2011 -0700 Merge pull request #1 from pabelanger/master Reformatting per PEP 8 recommendations Signed-off-by: Paul Belanger Acked-by: Russell Bryant commit b60603ecc4fd01efda084755a5be683805b0ce7e Author: pabelanger Date: Wed May 4 17:11:23 2011 -0400 Reformatting per PEP 8 recommendations commit 12f426adacd547cfc55148b4808726e05373d2e4 Author: Paul Belanger Date: Mon Apr 18 17:58:06 2011 +0000 Formatting changes; :retab to remove red blobs. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@47 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 0caa65a8c23b816e5a9acdbc6c6f2708b3d34091 Author: Paul Belanger Date: Mon Apr 18 17:53:54 2011 +0000 Formatting changes; :retab to remove red blobs. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@46 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 685bdd14f2098ef7b283c421f50e1cb17cf54ded Author: Paul Belanger Date: Mon Apr 18 17:51:37 2011 +0000 Formatting changes; :retab to remove red blobs git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@45 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 9a43f60d67aeda7c43ee08ee656e1d4ad1c6475c Author: Russell Bryant Date: Mon Dec 20 16:14:26 2010 +0000 Add ParkedCall and UnParkedCall manager events. Patch from pabelanger. (closes issue #3139240) git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@44 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit df5ce590e09348e48214a8a2ac3ee170bb5b6b34 Author: Russell Bryant Date: Thu Dec 16 17:46:52 2010 +0000 Add an AMI instance identifier. (closes issue #3138667) git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@43 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 1ffce48174d57ae174ba3c8785a5dd932696f61a Author: Russell Bryant Date: Mon Nov 22 18:45:36 2010 +0000 Revert r41. The associated Asterisk changes have been reverted. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@42 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit fea77d601183009682fae3db633812f2eb584887 Author: Russell Bryant Date: Fri Nov 19 19:46:08 2010 +0000 Update waitForDigit() to match latest Asterisk changes. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@41 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 9af4bc08eb8970785ef752b20bad61809e931a03 Author: Russell Bryant Date: Mon Aug 16 16:19:27 2010 +0000 Allow passing back details from the RECORD FILE response. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@40 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 17db367cdb21a5cca58014d529b98e2a872ab158 Author: Russell Bryant Date: Tue Aug 10 19:01:57 2010 +0000 Trim spaces inside parens to be closer to PEP8 compatible. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@39 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 1cdea7ea9d8e81a8f0dc1880bbe477a992480da0 Author: Russell Bryant Date: Wed Aug 4 15:18:10 2010 +0000 Fix bogus "GET DATA" command when FastAGI.getData() is called with maxDigits=None. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@38 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit b03bbda6bc7a54d64fdab26e32009f82e0607dea Author: Russell Bryant Date: Tue Aug 3 15:33:51 2010 +0000 Add a compatibility fix, and a documentation update. For FastAGI.exectue(), add an argument that specifies that commas should be used as a separator instead of pipes. Add a documentation update for FastAGI.noop(), since it now can take an argument. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@37 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit c478f1e23e9d6d71683c16d1ad617b979ce75a19 Author: Russell Bryant Date: Tue Jul 27 20:33:30 2010 +0000 Change indentation from tabs to spaces (PEP8). Trim trailing whitespace. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@36 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit aac1abccc2aa4ddcb525ae345465a7edb562f4de Author: Russell Bryant Date: Tue Jul 27 20:30:44 2010 +0000 Add quotes around arguments in AGI commands to properly handle cases where arguments have spaces in them. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@35 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit e268d16eaee79809bf7cb506837fc7adb7267793 Author: Russell Bryant Date: Mon Jul 26 16:13:19 2010 +0000 Fix a problem with FastAGIProtocol.controlStreamFile(). git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@34 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 9b224c64de2d09ed343e70aa690e802d8fd77570 Author: Russell Bryant Date: Thu Apr 1 22:47:29 2010 +0000 Fix expected response to a Ping on Asterisk 1.6 and above. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@33 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit cfbf21b265cf36f296884250c1f69f8830ced0c4 Author: Russell Bryant Date: Thu Apr 1 17:32:09 2010 +0000 Strip trailing whitespace. (Testing commit access) git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@32 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit d208feb270579ae862a353c2feac3ca4c024aafa Author: Mike C. Fletcher Date: Mon Jan 26 18:55:57 2009 +0000 Version bump to beta-1, add release notes git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@31 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 4756baa3a851c41da44cecf8f7028984f3c10f18 Author: Godson Gera Date: Fri Oct 10 07:28:44 2008 +0000 bug fix of playback app in fastagi git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@30 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit f881b812a2c9a3e5d34fad3bb8e716ae6a761514 Author: Godson Gera Date: Fri Oct 10 07:27:37 2008 +0000 Added new manager commands currently available in asterisk 1.4 also bug fix of playback app in fastagi git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@29 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 556c8fa89e9f2b65e54e4e3768890c49e7b8826a Author: Mike C. Fletcher Date: Fri May 30 17:58:06 2008 +0000 Update teh range of values git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@28 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit e01a6cd5cf1fe5d1a29571e7ca64960fa68c861f Author: Mike C. Fletcher Date: Mon Mar 26 13:49:55 2007 +0000 Update release notes git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@27 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 86e6dc280db0992dcab76fa617dc82be0dd374b5 Author: Mike C. Fletcher Date: Mon Mar 26 13:42:10 2007 +0000 Version bump git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@26 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 05830869e433e5889b193190d3b01f3d1ad0579f Author: Mike C. Fletcher Date: Mon Mar 26 13:41:48 2007 +0000 Bug fixes for clientConnectionFailed and menu callback registration git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@25 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 3b45ae1c05c659d328ee098dc40fb9ae2dc31be5 Author: Mike C. Fletcher Date: Thu Feb 15 05:39:34 2007 +0000 Version notes git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@24 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit c3184771712b5f122a2ff7b2bd63b717445199b7 Author: Mike C. Fletcher Date: Thu Feb 15 05:39:17 2007 +0000 Version bump git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@23 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit d5d1e697de4180a0baf33247b6ac53b58e28b9ea Author: Mike C. Fletcher Date: Thu Feb 15 01:35:21 2007 +0000 Connection timeout handler git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@22 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 8214d72bdbadb88fe4eba903090f50eec0c421c6 Author: Mike C. Fletcher Date: Thu Feb 15 01:34:51 2007 +0000 Timeout for AMI connections. Allow for overriding the load configuration operation git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@21 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 5011c68dce13cda6a5369bf8733a3d3e5de287c2 Author: Mike C. Fletcher Date: Thu Feb 15 01:33:51 2007 +0000 Fix bug in set extensions git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@20 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 47ca19a6095f004709699f3ab9ad11cae55a0ead Author: Mike C. Fletcher Date: Thu Feb 15 01:32:54 2007 +0000 Added prompt-as-runner function and collect audio feature git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@19 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 3ee0333be3fa793339de36bbe7544b140515b7a8 Author: Mike C. Fletcher Date: Fri Sep 1 18:27:01 2006 +0000 Missing string git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@18 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 80f2e7095bdfedf72b307e199a9a991b5339e12d Author: Mike C. Fletcher Date: Fri Sep 1 17:27:18 2006 +0000 Add a playback method as suggested by Yoann Aubineau git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@17 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit c72fb2d6b1f0b9c13a4bc85f68edefa8edbb7796 Author: Mike C. Fletcher Date: Fri Sep 1 17:17:58 2006 +0000 Fix escaping problem reported by Yoann Aubineau for setVariable calls. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@16 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit c5b3f4f2c31ee28773219ff5f59bb0e067536e05 Author: Mike C. Fletcher Date: Wed Aug 30 17:05:29 2006 +0000 Yoann Aubineau (yoann.aubineau@wengo.fr)'s patches git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@15 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 6c3ce1283865e6d3ca0057c7500ede0e579bedc2 Author: Mike C. Fletcher Date: Wed Aug 30 16:56:52 2006 +0000 Add support for waiting for a fastagi connection to be dropped. Fix missing import error. Convert all manager API message keys/values to strings before sending. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@14 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit f40d83032901ab485db7a149ea05157e0653f702 Author: Mike C. Fletcher Date: Fri Apr 28 03:39:31 2006 +0000 Addition of the prompt system. Version bump. Minor fix in onStreamingComplete git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@11 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit c9cdf4a3b94ac490f84b93c8d1e21fd53db7a525 Author: Mike C. Fletcher Date: Thu Apr 27 11:58:31 2006 +0000 version bump git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@9 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 5ba2d9d4b7de297f057ce0213a0ea917d8321ce4 Author: Mike C. Fletcher Date: Thu Apr 27 11:58:01 2006 +0000 Password-collection menu options. Allow options to declare an onSuccess handler to be called before return. git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@8 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 9cf1e59c19556d59e47fa3e525296e4709b7e352 Author: Mike C. Fletcher Date: Thu Apr 6 02:08:04 2006 +0000 Change the homepage git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@7 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 446a842f1da1328ed62c96794d9db4a1554968f8 Author: Mike C. Fletcher Date: Thu Apr 6 01:57:10 2006 +0000 The missing examples git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@6 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 9e848fe0de22c1fbe66ce5d9acfd60bfa94d741a Author: Mike C. Fletcher Date: Thu Apr 6 01:31:15 2006 +0000 Note regarding checking out the svn version git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@5 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 072f4c6cfed9f8211140a1d167ebbb7550e2681c Author: Mike C. Fletcher Date: Thu Apr 6 01:28:07 2006 +0000 Changes up to 1.0.7+, i.e. what we had previously git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@4 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 5180c6cfced1489b8f2b31b707bbafac5f3d0d05 Author: Mike C. Fletcher Date: Thu Apr 6 01:17:31 2006 +0000 SourceForge alterations git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@3 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit 932c06c71dc53c06d5ae5909aa6159b9f641d4c8 Author: Mike C. Fletcher Date: Thu Apr 6 01:08:42 2006 +0000 Create an upload script to get around the extremely annoying sf group limitation git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@2 b6236cd2-a710-0410-8d3e-adb8ec5871e9 commit c4765f052f86edd5e3ff271b85b80b1d771e1a0c Author: Mike C. Fletcher Date: Wed Apr 5 05:18:10 2006 +0000 Initial SourceForge import/upload git-svn-id: https://starpy.svn.sourceforge.net/svnroot/starpy/trunk@1 b6236cd2-a710-0410-8d3e-adb8ec5871e9 starpy-1.0.1/LICENSE000066400000000000000000000031031171675153000140430ustar00rootroot00000000000000Copyright (c) 2006, Michael C. Fletcher All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of Michael C. Fletcher, or the name of any Contributor, may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY SITUATION ENDANGERING HUMAN LIFE OR PROPERTY. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. starpy-1.0.1/MANIFEST.in000066400000000000000000000010011171675153000145670ustar00rootroot00000000000000include 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.* starpy-1.0.1/README000066400000000000000000000000071171675153000137160ustar00rootroot00000000000000StarPy starpy-1.0.1/UPGRADE.txt000066400000000000000000000005711171675153000146740ustar00rootroot00000000000000UPGRADE notes for starpy python module ====================================== - References to zaptel functions have been converted into their DAHDI equivalents. For most of the functions, the first three letters of the old function names "zap", have been removed and replaced with "dahdi". i.e. zapDNDoff is now dahdiDNDoff See CHANGES.txt for a complete list. starpy-1.0.1/__init__.py000066400000000000000000000011261171675153000151520ustar00rootroot00000000000000"""Twisted Protocols for Communication with the Asterisk PBX StarPy allows you to communicate with an Asterisk PBX using an Asterisk Manager Interface (AMI) client or a Fast Asterisk Gateway Interface (FastAGI) server. The protocols are designed to be included in applications that want to allow for multi-protocol communication using the Twisted protocol. Their integration with Asterisk does not require any modification to the Asterisk source code (though a manager account is obviously required for the AMI interface, and you have to actually call the FastAGI server from the dialplan). """ starpy-1.0.1/doc/000077500000000000000000000000001171675153000136065ustar00rootroot00000000000000starpy-1.0.1/doc/index.html000066400000000000000000000566751171675153000156260ustar00rootroot00000000000000 StarPy Asterisk Protocols for Twisted

StarPy Asterisk Protocols for Twisted

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

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

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

Installation

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

python setup.py install

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

svn co https://svn.sourceforge.net/svnroot/starpy/trunk starpy

On your PythonPath.

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

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

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

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

Asterisk Manager Interface (AMI) Usage

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fast Asterisk Gateway Interface (FastAGI) Usage

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

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

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

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

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

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

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

See the examples directory for examples of FastAGI scripts.

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

Sequential Operations

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

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

Menu Objects Usage

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

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

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

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

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

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

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

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

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

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

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

Signalling Errors from FastAGI

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

df.addErrback( agi.jumpOnError, 100 )

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

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

Secondary Services

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

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

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

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

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

Simple FastAGI Application Example

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

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

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

log = logging.getLogger( 'hellofastagi' )

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

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

Changes

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

  • 1.0.0b1
    • Provide download link in setup.py to allow easy-install to work
  • 1.0.0a13
    • Godson's Asterisk 1.4.x interface updates
  • 1.0.0.a12
    • Fix "recursion" bug in menu's onReadMenu
    • Fix bug in clientConnectionFailed parameters for AMI connections
  • 1.0.0.a11
    • Fix bug in fastagi setExtension
    • Add timeout to manager api
    • Allow for overriding utilapplication configuration loading (to add new sections, for instance)
  • 1.0.0.a10
    • Allow for registering a FastAGI handler for None, is used to provide a default handler for extensions which do not have an explicit waiter or handler registered
  • 1.0.0.a9
    • Add multi-element prompt capability, so you can use sound files, numbers, read alpha and similar operations to define a compound prompt for a menu.

      Note: This change breaks all previously defined menus, you need to change the "soundFile" property of your menus to be "prompt".  You may (but do not need to) wrap your sound file names in a menu.AudioPrompt() instance.
    • Minor bug in onStreamingComplete fixed (variable name shadowed the module)
  • 1.0.0.a8
    • Add password-checking menu operation
    • Add ability to pass an "onSuccess" handler to a menu option; it is called before returning from selection of that option
    • Fix bug in AMI handling of multi-line command results that include ':' characters
    • Add example showing usage of ami.command(...)
  • 1.0.0.a7
    • Fix bug introduced in a6 where None could no longer be used to handle all events in AMI
  • 1.0.0.a6
    • Fix bug in AMIProtocol.deregisterEvent, would remove all registrations in all instances
    • Add ability to register/deregister multiple events at once in AMIProtocol.registerEvent and deregisterEvent
  • 1.0.0a5
    • Setup script bug-fix for placement of data files (one directory level too high)
    • Minor documentation enhancements
    • priexhaustion.py example application added (track total number of open channels)
    • Bug-fix in AGI getVariable (incorrect/incomplete parsing)
    • Trivial bug-fix in hellofastagiapp.py (editing problem during documentation creation)
  • 1.0.0a4
    • Fixed naming error on setPriority
    • Added jumpOnError
    • Fixes for Call Duration Sample to work with newest code
    • More documentation
  • 1.0.0a3
    • FastAGI's getOption API change, should actually be useful now
    • IVR Menu and CollectDigits objects first release
  • 1.0.0a2
    • Slightly more mature release, a few minor applications have been built with the package to test out operation, a few bugs have been fixed.
    • Call Duration sample application added, note that this requires BasicProperty
  • 1.0.0a1
    • Initial release of the StarPy package, much of the functionality is still untested, but the coverage of the APIs should be close to complete.

License

StarPy is licensed under extremely liberal terms.

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

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

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

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

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

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

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

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

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

%s

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

Autosurvey Demo Application

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

Features:

  • Enter set of phone numbers or SIP addresses to contact
  • The owner of the survey is called
    • Owner can record the options
    • Owner can view/listen to the options from the web-form
  • Each user is called and presented with the survey
  • Results of the survey can be viewed on the web-form

Setup

Enter your phone number here:

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

Introduction

Options

Enter the participant's phone numbers here:

Results

starpy-1.0.1/examples/calldurationcallback.py000066400000000000000000000166301171675153000213750ustar00rootroot00000000000000#! /usr/bin/env python """Sample application to read call duration back to user Implemented as an AGI and a manager connection, send those who want to time the call to the AGI, we will wait for the end of the call, then call them back with the duration message. """ from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi import utilapplication import menu import os, logging, pprint, time log = logging.getLogger( 'callduration' ) class Application( utilapplication.UtilApplication ): """Application for the call duration callback mechanism""" def onS( self, agi ): """Incoming AGI connection to the "s" extension (start operation)""" log.info( """New call tracker""" ) c = CallTracker() return c.recordChannelInfo( agi ).addErrback( agi.jumpOnError, difference=100, ) class CallTracker( object ): """Object which tracks duration of a single call This object encapsulates the entire interaction with the user, from the initial incoming FastAGI that records the channel ID and account number through the manager watching for the disconnect to the new call setup and the FastAGI that plays back the results... Requires a context 'callduration' with 's' mapping to this AGI, as well as all numeric extensions. """ ourContext = 'callduration' def __init__( self ): """Initialise the tracker object""" self.uniqueChannelId = None self.currentChannel = None self.callbackChannel = None self.account = None self.cancelled = False self.ami = None self.startTime = None self.stopTime = None def recordChannelInfo( self, agi ): """Records relevant channel information, creates manager watcher""" self.uniqueChannelId = agi.variables['agi_uniqueid'] self.currentChannel = currentChannel = agi.variables['agi_channel'] # XXX everything up to the last - is normally our local caller's "address" # this is not, however, a great way to decide who to call back... self.callbackChannel = currentChannel.rsplit( '-', 1)[0] # Ask user for the account number... df = menu.CollectDigits( soundFile = 'your-account', maxDigits = 7, minDigits = 3, timeout = 5, )( agi ).addCallback( self.onAccountInput,agi=agi, ) # XXX handle AMI login failure... amiDF = APPLICATION.amiSpecifier.login( ).addCallback( self.onAMIConnect ) dl = defer.DeferredList( [df, amiDF] ) return dl.addCallback( self.onConnectAndAccount ) def onAccountInput( self, result, agi, retries=2): """Allow user to enter again if timed out""" self.account = result[0][1] self.startTime = time.time() agi.finish() # let the user go about their business... return agi def cleanUp( self, agi=None ): """Cleanup on error as much as possible""" items = [] if self.ami: items.append( self.ami.logoff()) self.ami = None if items: return defer.DeferredList( items ) else: return defer.succeed( False ) def onAMIConnect( self, ami ): """We have successfully connected to the AMI""" log.debug( "AMI login complete" ) if not self.cancelled: self.ami = ami return ami else: return self.ami.logoff() def onConnectAndAccount( self, results ): """We have connected and retrieved an account""" log.info( """AMI Connected and account information gathered: %s""", self.uniqueChannelId ) df = defer.Deferred() def onChannelHangup( ami, event ): """Deal with the hangup of an event""" if event['uniqueid'] == self.uniqueChannelId: log.info( """AMI Detected close of our channel: %s""", self.uniqueChannelId ) self.stopTime = time.time() # give the user a few seconds to put down the hand-set reactor.callLater( 2, df.callback, event ) self.ami.deregisterEvent( 'Hangup', onChannelHangup ) log.debug( 'event:', event ) if not self.cancelled: self.ami.registerEvent( 'Hangup', onChannelHangup ) return df.addCallback( self.onHangup, callbacks=5 ) def onHangup( self, event, callbacks=5 ): """Okay, the call is finished, time to inform the user""" log.debug( 'onHangup %s %s', event, callbacks ) def ignoreResult( result ): """Since we're using an equal timeout waiting for a connect we don't care *how* this fails/succeeds""" pass self.ami.originate( self.callbackChannel, self.ourContext, id(self), 1, timeout = 15, ).addCallbacks( ignoreResult, ignoreResult ) df = APPLICATION.waitForCallOn( id(self), 15 ) df.addCallbacks( self.onUserReconnected, self.onUserReconnectFail, errbackKeywords = { 'event': event, 'callbacks': callbacks-1 }, ) def onUserReconnectFail( self, reason, event, callbacks ): """Wait for bit, then retry...""" if callbacks: # XXX really want something like a decaying back-off in frequency # with final values of e.g. an hour... log.info( """Failure connecting: will retry in 30 seconds""" ) reactor.callLater( 30, self.onHangup, event, callbacks ) else: log.error( """Unable to connect to user, giving up""" ) return self.cleanUp( None ) def onUserReconnected( self, agi ): """Handle the user interaction after they've re-connected""" log.info( """Connection re-established with the user""" ) # XXX should handle unexpected failures in here... delta = self.stopTime - self.startTime minutes, seconds = divmod( delta, 60 ) seconds = int(seconds) hours, minutes = divmod( minutes, 60 ) duration = [] if hours: duration.append( '%s hour%s'%(hours,['','s'][hours!=1])) if minutes: duration.append( '%s second%s'%(minutes,['','s'][minutes!=1])) if seconds: duration.append( '%s second%s'%(seconds,['','s'][seconds!=1])) if not duration: duration = '0' else: duration = " ".join( duration ) seq = fastagi.InSequence( ) seq.append( agi.wait, 1 ) seq.append( agi.execute, "Festival", "Call to account %r took %s"%(self.account,duration) ) seq.append( agi.wait, 1 ) seq.append( agi.execute, "Festival", "Repeating, call to account %r took %s"%(self.account,duration) ) seq.append( agi.wait, 1 ) seq.append( agi.finish ) def logSuccess( ): log.debug( """Finished successfully!""" ) return defer.succeed( True ) seq.append( logSuccess ) seq.append( self.cleanUp, agi ) return seq() APPLICATION = Application() if __name__ == "__main__": logging.basicConfig() log.setLevel( logging.DEBUG ) #manager.log.setLevel( logging.DEBUG ) #fastagi.log.setLevel( logging.DEBUG ) APPLICATION.handleCallsFor( 's', APPLICATION.onS ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) from twisted.internet import reactor reactor.run() starpy-1.0.1/examples/calldurationextensions.conf000066400000000000000000000011251171675153000223260ustar00rootroot00000000000000; Extensions to allow the autosurvey example application ; to run on the system... include into your extensions.conf ; with a line like: ; #include /home/mcfletch/pylive/starpy/examples/calldurationextensions.conf ; You need to Goto(callduration,s,1) for those calls for which you want to have ; callduration support for [regulardial] exten => s,1,Dial(SIP/3333@testout) exten => s,2,Hangup() [callduration] exten => s,1,Answer() exten => s,2,AGI(agi://localhost:4576) exten => s,3,Goto(regulardial,s,1) exten => _X.,1,Answer() exten => _X.,2,AGI(agi://localhost:4576) exten => _X.,3,Hangup() starpy-1.0.1/examples/connecttoivr.py000066400000000000000000000021061171675153000177450ustar00rootroot00000000000000"""Example script to generate a call to connect a remote channel to an IVR""" from starpy import manager from twisted.internet import reactor import sys, logging def main( channel = 'sip/20035@aci.on.ca', connectTo=('outgoing','s','1') ): f = manager.AMIFactory(sys.argv[1], sys.argv[2]) df = f.login() def onLogin( protocol ): """On Login, attempt to originate the call""" context, extension, priority = connectTo df = protocol.originate( channel, context,extension,priority, ) def onFinished( result ): df = protocol.logoff() def onLogoff( result ): reactor.stop() return df.addCallbacks( onLogoff, onLogoff ) def onFailure( reason ): print reason.getTraceback() return reason df.addErrback( onFailure ) df.addCallbacks( onFinished, onFinished ) return df def onFailure( reason ): """Unable to log in!""" print reason.getTraceback() reactor.stop() df.addCallbacks( onLogin, onFailure ) return df if __name__ == "__main__": manager.log.setLevel( logging.DEBUG ) logging.basicConfig() reactor.callWhenRunning( main ) reactor.run() starpy-1.0.1/examples/connecttoivrapp.py000066400000000000000000000021021171675153000204420ustar00rootroot00000000000000"""Example script to generate a call to connect a remote channel to an IVR This version of the script uses the utilapplication framework and is pared down for presentation on a series of slides """ from starpy import manager import utilapplication from twisted.internet import reactor import sys, logging APPLICATION = utilapplication.UtilApplication() def main( channel = 'sip/4167290048@testout', connectTo=('outgoing','s','1') ): df = APPLICATION.amiSpecifier.login() def onLogin( protocol ): """We've logged into the manager, generate a call and log off""" context, extension, priority = connectTo df = protocol.originate( channel, context,extension,priority, ) def onFinished( result ): return protocol.logoff() df.addCallbacks( onFinished, onFinished ) return df def onFailure( reason ): print reason.getTraceback() def onFinished( result ): reactor.stop() df.addCallbacks( onLogin, onFailure ).addCallbacks( onFinished, onFinished ) return df if __name__ == "__main__": logging.basicConfig() reactor.callWhenRunning( main ) reactor.run() starpy-1.0.1/examples/fastagisetvariable.py000066400000000000000000000015521171675153000210740ustar00rootroot00000000000000#! /usr/bin/env python """Try to set a FastAGI variable""" from twisted.internet import reactor from starpy import fastagi import utilapplication import logging, time log = logging.getLogger( 'hellofastagi' ) def testFunction( agi ): """Demonstrate simplistic use of the AGI interface with sequence of actions""" log.debug( 'testFunction' ) def setX( ): return agi.setVariable( 'this"toset', 'That"2set' ) def getX( result ): return agi.getVariable( 'this"toset' ) def onX( value ): print 'Retrieved value', value reactor.stop() return setX().addCallback( getX ).addCallbacks( onX, onX ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.DEBUG ) APPLICATION = utilapplication.UtilApplication() APPLICATION.handleCallsFor( 's', testFunction ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) reactor.run() starpy-1.0.1/examples/getvariable.py000066400000000000000000000031041171675153000175140ustar00rootroot00000000000000#! /usr/bin/env python """Demonstrate usage of getVariable on the agi interface... """ from twisted.internet import reactor from starpy import fastagi import utilapplication import logging, time, pprint log = logging.getLogger( 'hellofastagi' ) def envVars( agi ): """Print out channel variables for display""" vars = [ x.split( ' -- ' )[0].strip() for x in agi.getVariable.__doc__.splitlines() if len(x.split( ' -- ' )) == 2 ] for var in vars: yield var def printVar( result, agi, vars ): """Print out the variables produced by envVars""" def doPrint( result, var ): print '%r -- %r'%( var, result ) def notAvailable( reason, var ): print '%r -- UNDEFINED'%( var, ) try: var = vars.next() except StopIteration, err: return None else: return agi.getVariable( var ).addCallback( doPrint, var ).addErrback( notAvailable, var, ).addCallback( printVar, agi, vars, ) def testFunction( agi ): """Print out known AGI variables""" log.debug( 'testFunction' ) print 'AGI Variables' pprint.pprint( agi.variables ) print 'Channel Variables' sequence = fastagi.InSequence() sequence.append( printVar, None, agi, envVars(agi) ) sequence.append( agi.finish ) def onFailure( reason ): log.error( "Failure: %s", reason.getTraceback()) agi.finish() return sequence().addErrback( onFailure ) if __name__ == "__main__": logging.basicConfig() #fastagi.log.setLevel( logging.DEBUG ) APPLICATION = utilapplication.UtilApplication() APPLICATION.handleCallsFor( 's', testFunction ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) reactor.run() starpy-1.0.1/examples/hellofastagi.py000066400000000000000000000014411171675153000176730ustar00rootroot00000000000000#! /usr/bin/env python """Simple FastAGI server using starpy""" from twisted.internet import reactor from starpy import fastagi import logging, time log = logging.getLogger( 'hellofastagi' ) def testFunction( agi ): """Demonstrate simplistic use of the AGI interface with sequence of actions""" log.debug( 'testFunction' ) sequence = fastagi.InSequence() sequence.append( agi.sayDateTime, time.time() ) sequence.append( agi.finish ) def onFailure( reason ): log.error( "Failure: %s", reason.getTraceback()) agi.finish() return sequence().addErrback( onFailure ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.DEBUG ) f = fastagi.FastAGIFactory(testFunction) reactor.listenTCP(4573, f, 50, '127.0.0.1') # only binding on local interface reactor.run() starpy-1.0.1/examples/hellofastagiapp.py000066400000000000000000000020351171675153000203740ustar00rootroot00000000000000#! /usr/bin/env python """FastAGI server using starpy and the utility application framework This is basically identical to hellofastagi, save that it uses the application framework to allow for configuration-file-based setup of the AGI service. """ from twisted.internet import reactor from starpy import fastagi import utilapplication import logging, time log = logging.getLogger( 'hellofastagi' ) def testFunction( agi ): """Demonstrate simplistic use of the AGI interface with sequence of actions""" log.debug( 'testFunction' ) sequence = fastagi.InSequence() sequence.append( agi.sayDateTime, time.time() ) sequence.append( agi.finish ) def onFailure( reason ): log.error( "Failure: %s", reason.getTraceback()) agi.finish() return sequence().addErrback( onFailure ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.DEBUG ) APPLICATION = utilapplication.UtilApplication() APPLICATION.handleCallsFor( 's', testFunction ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) reactor.run() starpy-1.0.1/examples/menu.py000066400000000000000000000633161171675153000162060ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """IVR-based menuing system with retry, exit, and similar useful features You use the menuing system by instantiating Interaction and Option sub-classes as a tree of options that make up an IVR menu. Calling the top-level menu produces a Deferred that fires with a list of [(Option,value),...] pairs, where Option is the thing chosen and value is the value entered by the user for choosing that option. When programming an IVR you will likely want to make Option sub-classes that are callable to accomplish the task indicated by the user. XXX allow for starting the menu system anywhere in the hierarchy XXX add the reject/accept menus to the CollectDigits (requires soundfiles in standard locations on the server, complicates install) """ from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi, error import utilapplication import os, logging, pprint, time from basicproperty import common, propertied, basic log = logging.getLogger('menu') log.setLevel(logging.DEBUG) class Interaction(propertied.Propertied): """Base class for user-interaction operations""" ALL_DIGITS = '0123456789*#' timeout = common.FloatProperty( "timeout", """Duration to wait for response before repeating message""", defaultValue = 5, ) maxRepetitions = common.IntegerProperty( "maxRepetitions", """Maximum number of times to play before failure""", defaultValue = 5, ) onSuccess = basic.BasicProperty( "onSuccess", """Optional callback for success with signature method( result, runner )""", ) onFailure = basic.BasicProperty( "onFailure", """Optional callback for failure with signature method( result, runner )""", ) runnerClass = None def __call__(self, agi, *args, **named): """Initiate AGI-based interaction with the user""" return self.runnerClass(model=self, agi=agi)(*args, **named) class Runner(propertied.Propertied): """User's interaction with a given Interaction-type""" agi = basic.BasicProperty( "agi", """The AGI instance we use to communicate with the user""", ) def defaultFinalDF(prop, client): """Produce the default finalDF with onSuccess/onFailure support""" df = defer.Deferred() model = client.model if hasattr(model, 'onSuccess'): log.debug('register onSuccess: %s', model.onSuccess) df.addCallback(model.onSuccess, runner=client) if hasattr(model, 'onFailure'): log.debug('register onFailure: %s', model.onFailure) df.addErrback(model.onFailure, runner=client) return df finalDF = basic.BasicProperty( "finalDF", """Final deferred we will callback/errback on success/failure""", defaultFunction = defaultFinalDF, ) del defaultFinalDF alreadyRepeated = common.IntegerProperty( "alreadyRepeated", """Number of times we've repeated the message...""", defaultValue = 0, ) model = basic.BasicProperty( "model", """The data-model that we are presenting to the user (e.g. Menu)""", ) def returnResult(self, result): """Return result of deferred to our original caller""" log.debug('returnResult: %s %s', self.model,result) if not self.finalDF.called: self.finalDF.debug = True self.finalDF.callback(result) else: log.debug('finalDF already called, ignoring %s', result) return result def returnError(self, reason): """Return failure of deferred to our original caller""" log.debug('returnError: %s', self.model) if not isinstance(reason.value, error.MenuExit): log.warn("""Failure during menu: %s""", reason.getTraceback()) if not self.finalDF.called: self.finalDF.debug = True self.finalDF.errback(reason) else: log.debug('finalDF already called, ignoring %s', reason.getTraceback()) def promptAsRunner(self, prompt): """Take set of prompt-compatible objects and produce a PromptRunner for them""" realPrompt = [] for p in prompt: if isinstance(p, (str, unicode)): p = AudioPrompt(p) elif isinstance(p, int): p = NumberPrompt(p) elif not isinstance(p, Prompt): raise TypeError( """Unknown prompt element type on %r: %s"""%( p, p.__class__, )) realPrompt.append(p) return PromptRunner( elements = realPrompt, escapeDigits = self.escapeDigits, agi = self.agi, timeout = self.model.timeout, ) class CollectDigitsRunner(Runner): """User's single interaction to enter a set of digits Note: Asterisk is hard-coded to use # to exit the entry-mode... """ def __call__(self, *args, **named): """Begin the AGI processing for the menu""" self.readDigits() return self.finalDF def readDigits(self, result=None): """Begin process of reading digits from the user""" soundFile = getattr(self.model, 'soundFile', None) if soundFile: # easiest possibility, just read out the file... return self.agi.getData( soundFile, timeout=self.model.timeout, maxDigits = getattr(self.model, 'maxDigits', None), ).addCallback(self.onReadDigits).addErrback(self.returnError) else: raise NotImplemented("""Haven't got non-soundfile menus working yet""") self.agi.getData(self.menu. filename, timeout=2.000, maxDigits=None) def validEntry(self, digits): """Determine whether given digits are considered a "valid" entry""" minDigits = getattr(self.model, 'minDigits', None) if minDigits is not None: if len(digits) < minDigits: return False, 'Too few digits' return True, None def onReadDigits(self, (digits,timeout)): """Deal with succesful result from reading digits""" log.info("""onReadDigits: %r, %s""", digits, timeout) valid, reason = self.validEntry(digits) if (not digits) and (not timeout): # user pressed # raise error.MenuExit( self.model, """User cancelled entry of digits""", ) if not valid: if self.model.tellInvalid: # this should be a menu, letting the user decide to re-enter, # or cancel entry pass self.alreadyRepeated += 1 if self.alreadyRepeated >= self.model.maxRepetitions: log.warn("""User did not complete digit-entry for %s, timing out""", self.model) raise error.MenuTimeout( self.model, """User did not finish digit-entry in %s passes of collection""" % ( self.alreadyRepeated, ) ) return self.readDigits() else: # Yay, we got a valid response! return self.returnResult([(self, digits)]) class CollectPasswordRunner(CollectDigitsRunner): """Password-runner, checks validity versus expected value""" expected = common.StringLocaleProperty( "expected", """The value expected/required from the user for this run""", ) def __call__(self, expected, *args, **named): """Begin the AGI processing for the menu""" self.expected = expected return super(CollectPasswordRunner, self).__call__(*args, **named) def validEntry(self, digits): """Determine whether given digits are considered a "valid" entry""" for digit in self.model.escapeDigits: if digit in digits: raise error.MenuExit( self.model, """User cancelled entry of password""", ) if digits != self.expected: return False, "Password doesn't match" return True, None class CollectAudioRunner(Runner): """Audio-collection runner, records user audio to a file on the asterisk server""" escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from recording""", defaultFunction = lambda prop, client: client.model.escapeDigits, setDefaultOnGet = False, ) def __call__(self, *args, **named): """Begin the AGI processing for the menu""" self.readPrompt() return self.finalDF def readPrompt(self, result=None): """Begin process of reading audio from the user""" if self.model.prompt: # wants us to read a prompt to the user before recording... runner = self.promptAsRunner(self.model.prompt) runner.timeout = 0.1 return runner().addCallback(self.onReadPrompt).addErrback(self.returnError) else: return self.collectAudio().addErrback(self.returnError) def onReadPrompt(self, result): """We've finished reading the prompt to the user, check for escape""" log.info('Finished reading prompt for collect audio: %r', result) if result and result in self.escapeDigits: raise error.MenuExit( self.model, """User cancelled entry of audio during prompt""", ) else: return self.collectAudio() def collectAudio( self ): """We're supposed to record audio from the user with our model's parameters""" # XXX use a temporary file for recording the audio, then move to final destination log.debug('collectAudio') if hasattr(self.model, 'temporaryFile'): filename = self.model.temporaryFile else: filename = self.model.filename df = self.agi.recordFile( filename=filename, format=self.model.format, escapeDigits=self.escapeDigits, timeout=self.model.timeout, offsetSamples=None, beep=self.model.beep, silence=self.model.silence, ).addCallbacks( self.onAudioCollected, self.onAudioCollectFail, ) if hasattr(self.model, 'temporaryFile'): df.addCallback(self.moveToFinal) return df def onAudioCollected(self, result): """Process the results of collecting the audio""" digits, typeOfExit, endpos = result if typeOfExit in ('hangup', 'timeout'): # expected common-case for recording... return self.returnResult((self,(digits,typeOfExit,endpos))) elif typeOfExit =='dtmf': raise error.MenuExit( self.model, """User cancelled entry of audio""", ) else: raise ValueError("""Unrecognised recordFile results: (%s, %s %s)""" % ( digits, typeOfExit, endpos, )) def onAudioCollectFail(self, reason): """Process failure to record audio""" log.error( """Failure collecting audio for CollectAudio instance %s: %s""", self.model, reason.getTraceback(), ) return reason # re-raise the error... def moveToFinal(self, result): """On succesful recording, move temporaryFile to final file""" log.info( 'Moving recorded audio %r to final destination %r', self.model.temporaryFile, self.model.filename ) import os try: os.rename( '%s.%s' % (self.model.temporaryFile, self.model.format), '%s.%s' % (self.model.filename, self.model.format), ) except (OSError, IOError), err: log.error( """Unable to move temporary recording file %r to target file %r: %s""", self.model.temporaryFile, self.model.filename, # XXX would like to use getException here... err, ) raise return result class MenuRunner(Runner): """User's single interaction with a given menu""" def defaultEscapeDigits(prop, client): """Return the default escape digits for the given client""" if client.model.tellInvalid: escapeDigits = client.model.ALL_DIGITS else: escapeDigits = "".join([o.option for o in client.model.options]) return escapeDigits escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from prompts to choose option""", defaultFunction = defaultEscapeDigits, ) del defaultEscapeDigits # clean up namespace def __call__(self, *args, **named): """Begin the AGI processing for the menu""" self.readMenu() return self.finalDF def readMenu(self, result=None): """Read our menu to the user""" runner = self.promptAsRunner(self.model.prompt) return runner().addCallback(self.onReadMenu).addErrback(self.returnError) def onReadMenu(self, pressed): """Deal with succesful result from reading menu""" log.info("""onReadMenu: %r""", pressed) if not pressed: self.alreadyRepeated += 1 if self.alreadyRepeated >= self.model.maxRepetitions: log.warn("""User did not complete menu selection for %s, timing out""", self.model) if not self.finalDF.called: raise error.MenuTimeout( self.model, """User did not finish selection in %s passes of menu""" % ( self.alreadyRepeated, ) ) return None return self.readMenu() else: # Yay, we got an escape-key pressed for option in self.model.options: if pressed in option.option: if callable(option): # allow for chaining down into sub-menus and the like... # we return the result of calling the option via self.finalDF return defer.maybeDeferred(option, pressed, self).addCallbacks( self.returnResult, self.returnError ) elif hasattr(option, 'onSuccess'): return defer.maybeDeferred(option.onSuccess, pressed, self).addCallbacks( self.returnResult, self.returnError ) else: return self.returnResult([(option,pressed),]) # but it wasn't anything we expected... if not self.model.tellInvalid: raise error.MenuUnexpectedOption( self.model, """User somehow selected %r, which isn't a recognised option?""" % (pressed,), ) else: return self.agi.getOption( self.model.INVALID_OPTION_FILE, self.escapeDigits, timeout=0, ).addCallback(self.readMenu).addErrback(self.returnError) class Menu(Interaction): """IVR-based menu, returns options selected by the user and keypresses The Menu holds a collection of Option instances along with a prompt which presents those options to the user. The menu will attempt to collect the user's selected option up to maxRepetitions times, playing the prompt each time. If tellInvalid is true, will allow any character being pressed to stop the playback, and will tell the user if the pressed character is not recognised. Otherwise will simply ignore a pressed character which isn't part of an Option object's 'option' property. The menu will chain into callable Options, so that SubMenu and ExitOn can be used to produce effects such as multi-level menus with options to return to the parent menu level. Returns [(option,char(pressedKey))...] for each level of menu explored """ INVALID_OPTION_FILE = 'pm-invalid-option' prompt = common.ListProperty( "prompt", """(Set of) prompts to run, can be Prompt instances or filenames Used by the PromptRunner to produce prompt selections """, ) textPrompt = common.StringProperty( "textPrompt", """Textual prompt describing the option""", ) options = common.ListProperty( "options", """Set of options the user may select""", ) tellInvalid = common.IntegerProperty( "tellInvalid", """Whether to tell the user that their selection is unrecognised""", defaultValue = True, ) runnerClass = MenuRunner class Option(propertied.Propertied): """A single menu option that can be chosen by the user""" option = common.StringLocaleProperty( "option", """Keypad values which select this option (list of characters)""", ) class SubMenu(Option): """A menu-holding option, just forwards call to the held menu""" menu = basic.BasicProperty( "menu", """The sub-menu we are presenting to the user""", ) def __call__(self, pressed, parent): """Get result from the sub-menu, add ourselves into the result""" def onResult(result): log.debug("""Child menu %s result: %s""", self.menu, result) result.insert(0, (self,pressed)) return result def onFailure(reason): """Trap voluntary exit and re-start the parent menu""" reason.trap(error.MenuExit) log.warn("""Restarting parent menu: %s""", parent) return parent.model(parent.agi) return self.menu(parent.agi).addCallbacks(onResult, onFailure) class ExitOn(Option): """An option which exits from the current menu level""" def __call__(self, pressed, parent): """Raise a MenuExit error""" raise error.MenuExit( self, pressed, parent, """User selected ExitOn option""", ) class CollectDigits(Interaction): """Collects some number of digits (e.g. an extension) from user""" soundFile = common.StringLocaleProperty( "soundFile", """File (name) for the pre-recorded blurb""", ) textPrompt = common.StringProperty( "textPrompt", """Textual prompt describing the option""", ) readBack = common.BooleanProperty( "readBack", """Whether to read the entered value back to the user""", defaultValue = False, ) minDigits = common.IntegerProperty( "minDigits", """Minimum number of digits to collect (only restricted if specified)""", ) maxDigits = common.IntegerProperty( "maxDigits", """Maximum number of digits to collect (only restricted if specified)""", ) runnerClass = CollectDigitsRunner tellInvalid = common.IntegerProperty( "tellInvalid", """Whether to tell the user that their selection is unrecognised""", defaultValue = True, ) class CollectPassword(CollectDigits): """Collects some number of password digits from the user""" runnerClass = CollectPasswordRunner escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from password entry""", defaultValue = '', ) soundFile = common.StringLocaleProperty( "soundFile", """File (name) for the pre-recorded blurb""", defaultValue = 'vm-password', ) class CollectAudio(Interaction): """Collects audio file from the user""" prompt = common.ListProperty( "prompt", """(Set of) prompts to run, can be Prompt instances or filenames Used by the PromptRunner to produce prompt selections """, ) textPrompt = common.StringProperty( "textPrompt", """Textual prompt describing the option""", ) temporaryFile = common.StringLocaleProperty( "temporaryFile", """Temporary file into which to record the audio before moving to filename""", ) filename = common.StringLocaleProperty( "filename", """Final filename into which to record the file...""", ) deleteOnFail = common.BooleanProperty( "deleteOnFail", """Whether to delete failed attempts to record a file""", defaultValue = True ) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from recording the file""", defaultValue = '#*0123456789', ) timeout = common.FloatProperty( "timeout", """Duration to wait for recording (maximum record time)""", defaultValue = 60, ) silence = common.FloatProperty( "silence", """Duration to wait for recording (maximum record time)""", defaultValue = 5, ) beep = common.BooleanProperty( "beep", """Whether to play a "beep" sound at beginning of recording""", defaultValue = True, ) runnerClass = CollectAudioRunner class PromptRunner(propertied.Propertied): """Prompt formed from list of sub-prompts """ elements = common.ListProperty( "elements", """Sub-elements of the prompt to be presented""", ) agi = basic.BasicProperty( "agi", """The FastAGI instance we're controlling""", ) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from playing the prompt""", ) timeout = common.FloatProperty( "timeout", """Timeout on data-entry after completed reading""", ) def __call__(self): """Return a deferred that chains all of the sub-prompts in order Returns from the first of the sub-prompts that recevies a selection returns str(digit) for the key the user pressed """ return self.onNext(None) def onNext(self, result, index=0): """Process the next operation""" if result is not None: return result try: element = self.elements[index] except IndexError, err: # okay, do a waitForDigit from timeout seconds... return self.agi.waitForDigit(self.timeout).addCallback( self.processKey ).addCallback(self.processLast) else: df = element.read(self.agi, self.escapeDigits) df.addCallback(self.processKey) df.addCallback(self.onNext, index=index+1) return df def processKey(self, result): """Does the pressed key belong to escapeDigits?""" if isinstance(result, tuple): # getOption result... if result[1] == 0: # failure during load of the file... log.warn("""Apparent failure during load of audio file: %s""", self.value) result = 0 else: result = result[0] if isinstance(result, str): if result: result = ord(result) else: result = 0 if result: # None or 0 # User pressed a key during the reading... key = chr(result) if key in self.escapeDigits: log.info('Exiting early due to user press of: %r', key) return key else: # we don't warn user in this menu if they press an unrecognised key! log.info('Ignoring user keypress because not in escapeDigits: %r', key) # completed reading without any escape digits, continue reading return None def processLast(self,result): if result is None: result = '' return result class Prompt(propertied.Propertied): """A Prompt to be read to the user""" value = basic.BasicProperty( "value", """Filename to be read to the user""", ) def __init__(self, value, **named): named['value'] = value super(Prompt, self).__init__(**named) class AudioPrompt(Prompt): """Default type of prompt, reads a file""" def read(self, agi, escapeDigits): """Read the audio prompt to the user""" # There's no "say file" operation... return agi.getOption(self.value, escapeDigits, 0.001) class TextPrompt(Prompt): """Prompt produced via festival text-to-speech reader (built-in command)""" def read(self, agi, escapeDigits): return agi.execute("Festival", self.value, escapeDigits) class NumberPrompt(Prompt): """Prompt that reads a number as a number""" value = common.IntegerProperty( "value", """Integer numeral to read""", ) def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayNumber(self.value, escapeDigits) class DigitsPrompt(Prompt): """Prompt that reads a number as digits""" def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayDigits(self.value, escapeDigits) class AlphaPrompt(Prompt): """Prompt that reads alphabetic string as characters""" def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayAlpha(self.value, escapeDigits) class DateTimePrompt(Prompt): """Prompt that reads a date/time as a date""" format = basic.BasicProperty( "format", """Format in which to read the date to the user""", defaultValue = None ) def read(self, agi, escapeDigits): """Read the audio prompt to the user""" return agi.sayDateTime(self.value, escapeDigits, format=self.format) starpy-1.0.1/examples/menutest.py000066400000000000000000000050151171675153000170760ustar00rootroot00000000000000#! /usr/bin/env python """Sample application to test the menuing utility classes""" from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi, error import utilapplication import menu import os, logging, pprint, time log = logging.getLogger( 'menutest' ) mainMenu = menu.Menu( prompt = '/home/mcfletch/starpydemo/soundfiles/menutest-toplevel', #prompt = 'houston', textPrompt = '''Top level of the menu test example Pressing Star will exit this menu at any time. Options zero and pound will exit with those options selected. Option one will start a submenu. Option two will start a digit-collecting sub-menu. We'll tell you if you make an invalid selection here.''', options = [ menu.Option( option='0' ), menu.Option( option='#' ), menu.ExitOn( option='*' ), menu.SubMenu( option='1', menu = menu.Menu( prompt = '/home/mcfletch/starpydemo/soundfiles/menutest-secondlevel', #prompt = 'atlantic', textPrompt = '''A second-level menu in the menu test example Pressing Star will exit this menu at any time. Options zero and pound will exit the whole menu with those options selected. We won't tell you if you make an invalid selection here. ''', tellInvalid = False, # don't report incorrect selections options = [ menu.Option( option='0' ), menu.Option( option='#' ), menu.ExitOn( option='*' ), ], ), ), menu.SubMenu( option='2', menu = menu.CollectDigits( textPrompt = '''Digit collection example, Please enter three to 5 digits. ''', soundFile = '/home/mcfletch/starpydemo/soundfiles/menutest-digits', #soundFile = 'extension', maxDigits = 5, minDigits = 3, ), ), ], ) class Application( utilapplication.UtilApplication ): """Application for the call duration callback mechanism""" def onS( self, agi ): """Incoming AGI connection to the "s" extension (start operation)""" log.info( """New call tracker""" ) def onComplete( result ): log.info( """Final result: %r""", result ) agi.finish() return mainMenu( agi ).addCallbacks( onComplete, onComplete ) APPLICATION = Application() if __name__ == "__main__": logging.basicConfig() log.setLevel( logging.DEBUG ) #manager.log.setLevel( logging.DEBUG ) fastagi.log.setLevel( logging.DEBUG ) menu.log.setLevel( logging.DEBUG ) APPLICATION.handleCallsFor( 's', APPLICATION.onS ) APPLICATION.agiSpecifier.run( APPLICATION.dispatchIncomingCall ) from twisted.internet import reactor reactor.run() starpy-1.0.1/examples/menutestextensions.conf000066400000000000000000000005041171675153000215110ustar00rootroot00000000000000; Extensions to allow the menutest example application ; to run on the system... include into your extensions.conf ; with a line like: ; #include /home/mcfletch/pylive/starpy/examples/menutestextensions.conf [menutest] exten => s,1,Answer() exten => s,2,Wait(1) exten => s,3,AGI(agi://localhost:4575) exten => s,4,Hangup() starpy-1.0.1/examples/priexhaustion.py000066400000000000000000000077441171675153000201470ustar00rootroot00000000000000#! /usr/bin/env python """Sample application to watch for PRI exhaustion This script watches for events on the AMI interface, tracking the identity of open channels in order to track how many channels are being used. This would be used to send messages to an administrator when network capacity is being approached. Similarly, you could watch for spare capacity on the network and use that to decide whether to allow low-priority calls, such as peering framework or free-world-dialup calls to go through. """ from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi import utilapplication import menu import os, logging, pprint, time from basicproperty import common, propertied, basic log = logging.getLogger( 'priexhaustion' ) log.setLevel( logging.INFO ) class ChannelTracker( propertied.Propertied ): """Track open channels on the Asterisk server""" channels = common.DictionaryProperty( "channels", """Set of open channels on the system""", ) thresholdCount = common.IntegerProperty( "thresholdCount", """Storage of threshold below which we don't warn user""", defaultValue = 20, ) def main( self ): """Main operation for the channel-tracking demo""" amiDF = APPLICATION.amiSpecifier.login( ).addCallback( self.onAMIConnect ) # XXX do something useful on failure to login... def onAMIConnect( self, ami ): """Register for AMI events""" # XXX should do an initial query to populate channels... # XXX should handle asterisk reboots (at the moment the AMI # interface will just stop generating events), not a practical # problem at the moment, but should have a periodic check to be sure # the interface is still up, and if not, should close and restart log.debug( 'onAMIConnect' ) ami.status().addCallback( self.onStatus, ami=ami ) ami.registerEvent( 'Hangup', self.onChannelHangup ) ami.registerEvent( 'Newchannel', self.onChannelNew ) def interestingEvent( self, event, ami=None ): """Decide whether this channel event is interesting Real-world application would want to take only Zap channels, or only channels from a given context, or whatever other filter you want in order to capture *just* the scarce resource (such as PRI lines). Keep in mind that an "interesting" event must show up as interesting for *both* Newchannel and Hangup events or you will leak references/channels or have unknown channels hanging up. """ return True def onStatus( self, events, ami=None ): """Integrate the current status into our set of channels""" log.debug( """Initial channel status retrieved""" ) for event in events: self.onChannelNew( ami, event ) def onChannelNew( self, ami, event ): """Handle creation of a new channel""" log.debug( """Start on channel %s""", event ) if self.interestingEvent( event, ami ): opening = not self.channels.has_key( event['uniqueid'] ) self.channels[ event['uniqueid'] ] = event if opening: self.onChannelChange( ami, event, opening = opening ) def onChannelHangup( self, ami, event ): """Handle hangup of an existing channel""" if self.interestingEvent( event, ami ): try: del self.channels[ event['uniqueid']] except KeyError, err: log.warn( """Hangup on unknown channel %s""", event ) else: log.debug( """Hangup on channel %s""", event ) self.onChannelChange( ami, event, opening = False ) def onChannelChange( self, ami, event, opening=False ): """Channel count has changed, do something useful like enforcing limits""" if opening and len(self.channels) > self.thresholdCount: log.warn( """Current channel count: %s""", len(self.channels ) ) else: log.info( """Current channel count: %s""", len(self.channels ) ) APPLICATION = utilapplication.UtilApplication() if __name__ == "__main__": logging.basicConfig() #log.setLevel( logging.DEBUG ) #manager.log.setLevel( logging.DEBUG ) #fastagi.log.setLevel( logging.DEBUG ) tracker = ChannelTracker() reactor.callWhenRunning( tracker.main ) reactor.run() starpy-1.0.1/examples/priexhaustionbare.py000066400000000000000000000045411171675153000207710ustar00rootroot00000000000000#! /usr/bin/env python from twisted.application import service, internet from twisted.internet import reactor, defer from starpy import manager, fastagi import utilapplication import menu import os, logging, pprint, time from basicproperty import common, propertied, basic log = logging.getLogger( 'priexhaustion' ) log.setLevel( logging.INFO ) class ChannelTracker( propertied.Propertied ): """Track open channels on the Asterisk server""" channels = common.DictionaryProperty( "channels", """Set of open channels on the system""", ) thresholdCount = common.IntegerProperty( "thresholdCount", """Storage of threshold below which we don't warn user""", defaultValue = 20, ) def main( self ): """Main operation for the channel-tracking demo""" amiDF = APPLICATION.amiSpecifier.login( ).addCallback( self.onAMIConnect ) def onAMIConnect( self, ami ): ami.status().addCallback( self.onStatus, ami=ami ) ami.registerEvent( 'Hangup', self.onChannelHangup ) ami.registerEvent( 'Newchannel', self.onChannelNew ) def onStatus( self, events, ami=None ): """Integrate the current status into our set of channels""" log.debug( """Initial channel status retrieved""" ) for event in events: self.onChannelNew( ami, event ) def onChannelNew( self, ami, event ): """Handle creation of a new channel""" log.debug( """Start on channel %s""", event ) opening = not self.channels.has_key( event['uniqueid'] ) self.channels[ event['uniqueid'] ] = event if opening: self.onChannelChange( ami, event, opening = opening ) def onChannelHangup( self, ami, event ): """Handle hangup of an existing channel""" try: del self.channels[ event['uniqueid']] except KeyError, err: log.warn( """Hangup on unknown channel %s""", event ) else: log.debug( """Hangup on channel %s""", event ) self.onChannelChange( ami, event, opening = False ) def onChannelChange( self, ami, event, opening=False ): """Channel count has changed, do something useful like enforcing limits""" if opening and len(self.channels) > self.thresholdCount: log.warn( """Current channel count: %s""", len(self.channels ) ) else: log.info( """Current channel count: %s""", len(self.channels ) ) APPLICATION = utilapplication.UtilApplication() if __name__ == "__main__": logging.basicConfig() tracker = ChannelTracker() reactor.callWhenRunning( tracker.main ) reactor.run() starpy-1.0.1/examples/readingdigits.py000066400000000000000000000037451171675153000200570ustar00rootroot00000000000000#! /usr/bin/env python """Read digits from the user in various ways...""" from twisted.internet import reactor, defer from starpy import fastagi, error import logging, time log = logging.getLogger( 'hellofastagi' ) class DialPlan( object ): """Stupid little application to report how many times it's been accessed""" def __init__( self ): self.count = 0 def __call__( self, agi ): """Store the AGI instance for later usage, kick off our operations""" self.agi = agi return self.start() def start( self ): """Begin the dial-plan-like operations""" return self.agi.answer().addCallbacks( self.onAnswered, self.answerFailure ) def answerFailure( self, reason ): """Deal with a failure to answer""" log.warn( """Unable to answer channel %r: %s""", self.agi.variables['agi_channel'], reason.getTraceback(), ) self.agi.finish() def onAnswered( self, resultLine ): """We've managed to answer the channel, yay!""" self.count += 1 return self.agi.wait( 2.0 ).addCallback( self.onWaited ) def onWaited( self, result ): """We've finished waiting, tell the user the number""" return self.agi.sayNumber( self.count, '*' ).addErrback( self.onNumberFailed, ).addCallbacks( self.onFinished, self.onFinished, ) def onFinished( self, resultLine ): """We said the number correctly, hang up on the user""" return self.agi.finish() def onNumberFailed( self, reason ): """We were unable to read the number to the user""" log.warn( """Unable to read number to user on channel %r: %s""", self.agi.variables['agi_channel'], reason.getTraceback(), ) def onHangupFailure( self, reason ): """Failed trying to hang up""" log.warn( """Unable to hang up channel %r: %s""", self.agi.variables['agi_channel'], reason.getTraceback(), ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.DEBUG ) f = fastagi.FastAGIFactory(DialPlan()) reactor.listenTCP(4573, f, 50, '127.0.0.1') # only binding on local interface reactor.run() starpy-1.0.1/examples/timestamp.py000066400000000000000000000023521171675153000172360ustar00rootroot00000000000000#! /usr/bin/env python """Provide a trivial date-and-time service""" from twisted.internet import reactor from starpy import fastagi import logging, time log = logging.getLogger( 'dateandtime' ) def testFunction( agi ): """Give time for some time a bit in the future""" log.debug( 'testFunction' ) df = agi.streamFile( 'at-tone-time-exactly' ) def onFailed( reason ): log.error( "Failure: %s", reason.getTraceback()) return None def cleanup( result ): agi.finish() return result def onSaid( resultLine ): """Having introduced, actually read the time""" t = time.time() t2 = t+20.0 df = agi.sayDateTime( t2, format='HM' ) def onDateFinished( resultLine ): # now need to sleep until .5 seconds before the time df = agi.wait( t2-.5-time.time() ) def onDoBeep( result ): df = agi.streamFile( 'beep' ) return df return df.addCallback( onDoBeep ) return df.addCallback( onDateFinished ) return df.addCallback( onSaid ).addErrback( onFailed ).addCallbacks( cleanup, cleanup, ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.INFO ) f = fastagi.FastAGIFactory(testFunction) reactor.listenTCP(4574, f, 50, '127.0.0.1') # only binding on local interface reactor.run() starpy-1.0.1/examples/timestampapp.py000066400000000000000000000025501171675153000177370ustar00rootroot00000000000000#! /usr/bin/env python """Provide a trivial date-and-time service""" from twisted.internet import reactor from starpy import fastagi import utilapplication import logging, time log = logging.getLogger( 'dateandtime' ) def testFunction( agi ): """Give time for some time a bit in the future""" log.debug( 'testFunction' ) df = agi.streamFile( 'at-tone-time-exactly' ) def onFailed( reason ): log.error( "Failure: %s", reason.getTraceback()) return None def cleanup( result ): agi.finish() return result def onSaid( resultLine ): """Having introduced, actually read the time""" t = time.time() t2 = t+7.0 df = agi.sayDateTime( t2, format='HMS' ) def onDateFinished( resultLine ): # now need to sleep until .05 seconds before the time df = agi.wait( t2-.05-time.time() ) def onDoBeep( result ): df = agi.streamFile( 'beep' ) return df def waitTwo( result ): return agi.streamFile( 'thank-you-for-calling' ) return df.addCallback( onDoBeep ).addCallback( waitTwo ) return df.addCallback( onDateFinished ) return df.addCallback( onSaid ).addErrback( onFailed ).addCallbacks( cleanup, cleanup, ) if __name__ == "__main__": logging.basicConfig() fastagi.log.setLevel( logging.INFO ) APPLICATION = utilapplication.UtilApplication() reactor.callWhenRunning( APPLICATION.agiSpecifier.run, testFunction ) reactor.run() starpy-1.0.1/examples/utilapplication.py000066400000000000000000000205251171675153000204360ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """Class providing utility applications with common support code""" from basicproperty import common, propertied, basic, weak from ConfigParser import ConfigParser from starpy import fastagi, manager from twisted.internet import defer, reactor import logging,os log = logging.getLogger( 'app' ) class UtilApplication( propertied.Propertied ): """Utility class providing simple application-level operations FastAGI entry points are waitForCallOn and handleCallsFor, which allow for one-shot and permanant handling of calls for an extension (respectively), and agiSpecifier, which is loaded from configuration file (as specified in self.configFiles). """ amiSpecifier = basic.BasicProperty( "amiSpecifier", """AMI connection specifier for the application see AMISpecifier""", defaultFunction = lambda prop,client: AMISpecifier() ) agiSpecifier = basic.BasicProperty( "agiSpecifier", """FastAGI server specifier for the application see AGISpecifier""", defaultFunction = lambda prop,client: AGISpecifier() ) extensionWaiters = common.DictionaryProperty( "extensionWaiters", """Set of deferreds waiting for incoming extensions""", ) extensionHandlers = common.DictionaryProperty( "extensionHandlers", """Set of permanant callbacks waiting for incoming extensions""", ) configFiles = configFiles=('starpy.conf','~/.starpy.conf') def __init__( self ): """Initialise the application from options in configFile""" self.loadConfigurations() def loadConfigurations( self ): parser = self._loadConfigFiles( self.configFiles ) self._copyPropertiesFrom( parser, 'AMI', self.amiSpecifier ) self._copyPropertiesFrom( parser, 'FastAGI', self.agiSpecifier ) return parser def _loadConfigFiles( self, configFiles ): """Load options from configuration files given (if present)""" parser = ConfigParser( ) filenames = [ os.path.abspath( os.path.expandvars( os.path.expanduser( file ) )) for file in configFiles ] log.info( "Possible configuration files:\n\t%s", "\n\t".join(filenames) or None) filenames = [ file for file in filenames if os.path.isfile(file) ] log.info( "Actual configuration files:\n\t%s", "\n\t".join(filenames) or None) parser.read( filenames ) return parser def _copyPropertiesFrom( self, parser, section, client, properties=None ): """Copy properties from the config-parser's given section into client""" if properties is None: properties = client.getProperties() for property in properties: if parser.has_option( section, property.name ): try: value = parser.get( section, property.name ) setattr( client, property.name, value ) except (TypeError,ValueError,AttributeError,NameError), err: log( """Unable to set property %r of %r to config-file value %r: %s"""%( property.name, client, parser.get( section, property.name, 1), err, )) return client def dispatchIncomingCall( self, agi ): """Handle an incoming call (dispatch to the appropriate registered handler)""" extension = agi.variables['agi_extension'] log.info( """AGI connection with extension: %r""", extension ) try: df = self.extensionWaiters.pop( extension ) except KeyError, err: try: callback = self.extensionHandlers[ extension ] except KeyError, err: try: callback = self.extensionHandlers[ None ] except KeyError, err: log.warn( """Unexpected connection to extension %r: %s""", extension, agi.variables ) agi.finish() return try: return callback( agi ) except Exception, err: log.error( """Failure during callback %s for agi %s: %s""", callback, agi.variables, err ) # XXX return a -1 here else: if not df.called: df.callback( agi ) def waitForCallOn( self, extension, timeout=15 ): """Wait for an AGI call on extension given extension -- string extension for which to wait timeout -- duration in seconds to wait before defer.TimeoutError is returned to the deferred. Note that waiting callback overrides any registered handler; that is, if you register one callback with waitForCallOn and another with handleCallsFor, the first incoming call will trigger the waitForCallOn handler. returns deferred returning connected FastAGIProtocol or an error """ extension = str(extension) log.info( 'Waiting for extension %r for %s seconds', extension, timeout ) df = defer.Deferred( ) self.extensionWaiters[ extension ] = df def onTimeout( ): if not df.called: df.errback( defer.TimeoutError( """Timeout waiting for call on extension: %r"""%(extension,) )) reactor.callLater( timeout, onTimeout ) return df def handleCallsFor( self, extension, callback ): """Register permanant handler for given extension extension -- string extension for which to wait or None to define a default handler (that chosen if there is not explicit handler or waiter) callback -- callback function to be called for each incoming channel to the given extension. Note that waiting callback overrides any registered handler; that is, if you register one callback with waitForCallOn and another with handleCallsFor, the first incoming call will trigger the waitForCallOn handler. returns None """ if extension is not None: extension = str(extension) self.extensionHandlers[ extension ] = callback class AMISpecifier( propertied.Propertied ): """Manager interface setup/specifier""" username = common.StringLocaleProperty( "username", """Login username for the manager interface""", ) secret = common.StringLocaleProperty( "secret", """Login secret for the manager interface""", ) password = secret server = common.StringLocaleProperty( "server", """Server IP address to which to connect""", defaultValue = '127.0.0.1', ) port = common.IntegerProperty( "port", """Server IP port to which to connect""", defaultValue = 5038, ) timeout = common.FloatProperty( "timeout", """Timeout in seconds for an AMI connection timeout""", defaultValue = 5.0, ) def login( self ): """Login to the specified manager via the AMI""" theManager = manager.AMIFactory(self.username, self.secret) return theManager.login(self.server, self.port, timeout=self.timeout) class AGISpecifier( propertied.Propertied ): """Specifier of where we send the user to connect to our AGI""" port = common.IntegerProperty( "port", """IP port on which to listen""", defaultValue = 4573, ) interface = common.StringLocaleProperty( "interface", """IP interface on which to listen (local only by default)""", defaultValue = '127.0.0.1', ) context = common.StringLocaleProperty( "context", """Asterisk context to which to connect incoming calls""", defaultValue = 'survey', ) def run( self, mainFunction ): """Start up the AGI server with the given mainFunction""" f = fastagi.FastAGIFactory(mainFunction) return reactor.listenTCP(self.port, f, 50, self.interface) starpy-1.0.1/fastagi.py000066400000000000000000001101741171675153000150350ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """Asterisk FastAGI server for use from the dialplan You use an asterisk FastAGI like this from extensions.conf: exten => 1000,3,AGI(agi://127.0.0.1:4573,arg1,arg2) Where 127.0.0.1 is the server and 4573 is the port on which the server is listening. Module defines a standard Python logging module log 'FastAGI' """ from twisted.internet import protocol, reactor, defer from twisted.internet import error as tw_error from twisted.protocols import basic import socket import logging import time from starpy import error log = logging.getLogger('FastAGI') FAILURE_CODE = -1 class FastAGIProtocol(basic.LineOnlyReceiver): """Protocol for the interfacing with the Asterisk FastAGI application Attributes: variables -- for connected protocol, the set of variables passed during initialisation, keys are all-lower-case, set of variables returned for an Asterisk 1.2.1 installation on Gentoo on a locally connected channel: agi_network = 'yes' agi_request = 'agi://localhost' agi_channel = 'SIP/mike-ccca' agi_language = 'en' agi_type = 'SIP' agi_uniqueid = '1139871605.0' agi_callerid = 'mike' agi_calleridname = 'Mike Fletcher' agi_callingpres = '0' agi_callingani2 = '0' agi_callington = '0' agi_callingtns = '0' agi_dnid = '1' agi_rdnis = 'unknown' agi_context = 'testing' agi_extension = '1' agi_priority = '1' agi_enhanced = '0.0' agi_accountcode = '' # Internal: readingVariables -- whether the instance is still in initialising by reading the setup variables from the connection messageCache -- stores incoming variables pendingMessages -- set of outstanding messages for which we expect replies lostConnectionDeferred -- deferred firing when the connection is lost delimiter -- uses bald newline instead of carriage-return-newline XXX Lots of problems with data-escaping, no docs on how to escape special characters that I can see... """ readingVariables = False lostConnectionDeferred = None delimiter = '\n' def __init__(self, *args, **named): """Initialise the AMIProtocol, arguments are ignored""" self.messageCache = [] self.variables = {} self.pendingMessages = [] def connectionMade(self): """(Internal) Handle incoming connection (new AGI request) Initiates read of the initial attributes passed by the server """ log.info("New Connection") self.readingVariables = True def connectionLost(self, reason): """(Internal) Handle loss of the connection (remote hangup)""" log.info("""Connection terminated""") try: for df in self.pendingMessages: df.errback(tw_error.ConnectionDone( "FastAGI connection terminated")) finally: if self.lostConnectionDeferred: self.lostConnectionDeferred.errback(reason) del self.pendingMessages[:] def onClose(self): """Return a deferred which will fire when the connection is lost""" if not self.lostConnectionDeferred: self.lostConnectionDeferred = defer.Deferred() return self.lostConnectionDeferred def lineReceived(self, line): """(Internal) Handle Twisted's report of an incoming line from AMI""" log.debug('Line In: %r', line) if self.readingVariables: if not line.strip(): self.readingVariables = False self.factory.mainFunction(self) else: try: key, value = line.split(':', 1) value = value[1:].rstrip('\n').rstrip('\r') except ValueError, err: log.error("""Invalid variable line: %r""", line) else: self.variables[key.lower()] = value log.debug("""%s = %r""", key, value) else: try: df = self.pendingMessages.pop(0) except IndexError, err: log.warn("Line received without pending deferred: %r", line) else: if line.startswith('200'): line = line[4:] if line.lower().startswith('result='): line = line[7:] df.callback(line) else: # XXX parse out the error code try: errCode, line = line.split(' ', 1) errCode = int(errCode) except ValueError, err: errCode = 500 df.errback(error.AGICommandFailure(errCode, line)) def sendCommand(self, commandString): """(Internal) Send the given command to the other side""" log.info("Send Command: %r", commandString) commandString = commandString.rstrip('\n').rstrip('\r') df = defer.Deferred() self.pendingMessages.append(df) self.sendLine(commandString) return df def checkFailure(self, result, failure='-1'): """(Internal) Check for a failure-code, raise error if == result""" # result code may have trailing information... try: resultInt, line = result.split(' ', 1) except ValueError, err: resultInt = result if resultInt.strip() == failure: raise error.AGICommandFailure(FAILURE_CODE, result) return result def resultAsInt(self, result): """(Internal) Convert result to an integer value""" try: return int(result.strip()) except ValueError, err: raise error.AGICommandFailure(FAILURE_CODE, result) def secondResultItem(self, result): """(Internal) Retrieve the second item on the result-line""" return result.split(' ', 1)[1] def resultPlusTimeoutFlag(self, resultLine): """(Internal) Result followed by optional flag declaring timeout""" try: digits, timeout = resultLine.split(' ', 1) return digits.strip(), True except ValueError, err: return resultLine.strip(), False def dateAsSeconds(self, date): """(Internal) Convert date to asterisk-compatible format""" if hasattr(date, 'timetuple'): # XXX values seem to be off here... date = time.mktime(date.timetuple()) elif isinstance(date, time.struct_time): date = time.mktime(date) return date def onRecordingComplete(self, resultLine): """(Internal) Handle putative success Also watch for failure-on-load problems """ try: digit, exitType, endposStuff = resultLine.split(' ', 2) except ValueError, err: pass else: digit = int(digit) exitType = exitType.strip('()') endposStuff = endposStuff.strip() if endposStuff.startswith('endpos='): endpos = int(endposStuff[7:].strip()) return digit, exitType, endpos raise ValueError("Unexpected result on streaming completion: %r" % resultLine) def onStreamingComplete(self, resultLine, skipMS=0): """(Internal) Handle putative success Also watch for failure-on-load problems """ try: digit, endposStuff = resultLine.split(' ', 1) except ValueError, err: pass else: digit = int(digit) endposStuff = endposStuff.strip() if endposStuff.startswith('endpos='): endpos = int(endposStuff[7:].strip()) if endpos == skipMS: # "likely" an error according to the wiki, # we'll raise an error... raise error.AGICommandFailure(FAILURE_CODE, "End position %s == original position, " "result code %s" % (endpos, digit)) return digit, endpos raise ValueError("Unexpected result on streaming completion: %r" % resultLine) def jumpOnError(self, reason, difference=100, forErrors=None): """On error, jump to original priority+100 This is intended to be registered as an errBack on a deferred for an end-user application. It performs the Asterisk-standard-ish jump-on-failure operation, jumping to new priority of priority+difference. It also forces return to the same context and extension, in case some other piece of code has changed those. difference -- priority jump to execute forErrors -- if specified, a tuple of error classes to which this particular jump is limited (i.e. only errors of this type will generate a jump & disconnect) returns deferred from the InSequence of operations required to reset the address... """ if forErrors: if not isinstance(forErrors, (tuple, list)): forErrors = (forErrors,) reason.trap(*forErrors) sequence = InSequence() sequence.append(self.setContext, self.variables['agi_context']) sequence.append(self.setExtension, self.variables['agi_extension']) sequence.append(self.setPriority, int(self.variables['agi_priority']) + difference) sequence.append(self.finish) return sequence() # End-user API def finish(self): """Finish the AGI "script" (drop connection) This command simply drops the connection to the Asterisk server, which the FastAGI protocol interprets as a successful termination. Note: There *should* be a mechanism for sending a "result" code, but I haven't found any documentation for it. """ self.transport.loseConnection() def answer(self): """Answer the channel (go off-hook) Returns deferred integer response code """ return self.sendCommand("ANSWER").addCallback( self.checkFailure ).addCallback(self.resultAsInt) def channelStatus(self, channel=None): """Retrieve the current channel's status Result integers (from the wiki): 0 Channel is down and available 1 Channel is down, but reserved 2 Channel is off hook 3 Digits (or equivalent) have been dialed 4 Line is ringing 5 Remote end is ringing 6 Line is up 7 Line is busy Returns deferred integer result code This could be used to decide if we can forward the channel to a given user, or whether we need to shunt them off somewhere else. """ if channel: command = 'CHANNEL STATUS "%s"' % (channel) else: command = "CHANNEL STATUS" return self.sendCommand(command).addCallback( self.checkFailure, ).addCallback(self.resultAsInt) def 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( # docs don't show a failure case, actually self.checkFailure, failure='-1', ).addCallback(self.resultAsInt) def setCallerID(self, number): """Set channel's caller ID to given number returns deferred integer result code """ command = "SET CALLERID %s" % (number) return self.sendCommand(command).addCallback(self.resultAsInt) def setContext(self, context): """Move channel to given context (no error checking is performed) returns deferred integer result code """ command = """SET CONTEXT %s""" % (context,) return self.sendCommand(command).addCallback(self.resultAsInt) def setExtension(self, extension): """Move channel to given extension (or 'i' if invalid) The call will drop if neither the extension or 'i' are there. returns deferred integer result code """ command = """SET EXTENSION %s""" % (extension,) return self.sendCommand(command).addCallback(self.resultAsInt) def setMusic(self, on=True, musicClass=None): """Enable/disable and/or choose music class for channel's music-on-hold returns deferred integer result code """ command = """SET MUSIC %s""" % (['OFF', 'ON'][on],) if musicClass is not None: command += " %s" % (musicClass,) return self.sendCommand(command).addCallback(self.resultAsInt) def setPriority(self, priority): """Move channel to given priority or drop if not there returns deferred integer result code """ command = """SET PRIORITY %s""" % (priority,) return self.sendCommand(command).addCallback(self.resultAsInt) def setVariable(self, variable, value): """Set given channel variable to given value variable -- the variable name passed to the server value -- the variable value passed to the server, will have any '"' characters removed in order to allow for " quoting of the value. returns deferred integer result code """ value = '''"%s"''' % (str(value).replace('"', ''),) command = 'SET VARIABLE "%s" "%s"' % (variable, value) return self.sendCommand(command).addCallback(self.resultAsInt) def streamFile(self, filename, escapeDigits="", offset=0): """Stream given file until escapeDigits starting from offset returns deferred (str(digit), int(endpos)) for playback Note: streamFile is apparently unstable in AGI, may want to use execute('PLAYBACK', ...) instead (according to the Wiki) """ command = 'STREAM FILE "%s" %r' % (filename, escapeDigits) if offset is not None: command += ' %s' % (offset) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback(self.onStreamingComplete, skipMS=offset) def tddMode(self, on=True): """Set TDD mode on the channel if possible (ZAP only ATM) on -- ON (True), OFF (False) or MATE (None) returns deferred integer result code """ if on is True: on = 'ON' elif on is False: on = 'OFF' elif on is None: on = 'MATE' command = 'TDD MODE %s' % (on,) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', # failure ).addCallback( # planned eventual failure case (not capable) self.checkFailure, failure='0', ).addCallback( self.resultAsInt, ) def verbose(self, message, level=None): """Send a logging message to the asterisk console for debugging etc message -- text to pass level -- 1-4 denoting verbosity level returns deferred integer result code """ command = 'VERBOSE %r' % (message,) if level is not None: command += ' %s' % (level) return self.sendCommand(command).addCallback( self.resultAsInt, ) def waitForDigit(self, timeout): """Wait up to timeout seconds for single digit to be pressed timeout -- timeout in seconds or -1 for infinite timeout (Asterisk uses milliseconds) returns deferred 0 on timeout or digit """ timeout *= 1000 command = "WAIT FOR DIGIT %s" % (timeout,) return self.sendCommand(command).addCallback( self.checkFailure, failure='-1', ).addCallback( self.resultAsInt, ) def wait(self, duration): """Wait for X seconds (just a wrapper around callLater, doesn't talk to server) returns deferred which fires some time after duration seconds have passed """ df = defer.Deferred() reactor.callLater(duration, df.callback, 0) return df class InSequence(object): """Single-shot item creating a set of actions to run in sequence""" def __init__(self): self.actions = [] self.results = [] self.finalDF = None def append(self, function, *args, **named): """Append an action to the set of actions to process""" self.actions.append((function, args, named)) def __call__(self): """Return deferred that fires when finished processing all items""" return self._doSequence() def _doSequence(self): """Return a deferred that does each action in sequence""" finalDF = defer.Deferred() self.onActionSuccess(None, finalDF=finalDF) return finalDF def recordResult(self, result): """Record the result for later""" self.results.append(result) return result def onActionSuccess(self, result, finalDF): """Handle individual-action success""" log.debug('onActionSuccess: %s', result) if self.actions: action = self.actions.pop(0) log.debug('action %s', action) df = defer.maybeDeferred(action[0], *action[1], **action[2]) df.addCallback(self.recordResult) df.addCallback(self.onActionSuccess, finalDF=finalDF) df.addErrback(self.onActionFailure, finalDF=finalDF) return df else: finalDF.callback(self.results) def onActionFailure(self, reason, finalDF): """Handle individual-action failure""" log.debug('onActionFailure') reason.results = self.results finalDF.errback(reason) class FastAGIFactory(protocol.Factory): """Factory generating FastAGI server instances """ protocol = FastAGIProtocol def __init__(self, mainFunction): """Initialise the factory mainFunction -- function taking a connected FastAGIProtocol instance this is the function that's run when the Asterisk server connects. """ self.mainFunction = mainFunction starpy-1.0.1/manager.py000066400000000000000000001022031171675153000150230ustar00rootroot00000000000000# # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """Asterisk Manager Interface for the Twisted networking framework The Asterisk Manager Interface is a simple line-oriented protocol that allows for basic control of the channels active on a given Asterisk server. Module defines a standard Python logging module log 'AMI' """ from twisted.internet import protocol, reactor, defer from twisted.protocols import basic from twisted.internet import error as tw_error import socket import logging from 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 'actionid' in message: key = message['actionid'] callback = self.actionIDCallbacks.get(key) if callback: try: callback(message) except Exception, err: # XXX log failure here... pass # otherwise is a monitor message or something we didn't send... if 'event' in message: self.dispatchEvent(message) def dispatchEvent(self, event): """Given an incoming event, dispatch to registered handlers""" for key in (event['event'], None): try: handlers = self.eventTypeCallbacks[key] except KeyError, err: pass else: for handler in handlers: try: handler(self, event) except Exception, err: # would like the getException code here... log.error( 'Exception in event handler %s on event %s: %s', handler, event, err ) def generateActionId(self): """Generate a unique action ID Assumes that hostName must be unique among all machines which talk to a given AMI server. With that is combined the memory location of the protocol object (which should be machine-unique) and the count of messages that this manager has created so far. Generally speaking, you shouldn't need to know the action ID, as the protocol handles the management of them automatically. """ self.count += 1 return '%s-%s-%s' % (self.hostName, id(self), self.count) def sendDeferred(self, message): """Send with a single-callback deferred object Returns deferred that fires when a response to this message is received """ df = 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 'actionid' not in message: message['actionid'] = self.generateActionId() if responseCallback: self.actionIDCallbacks[message['actionid']] = responseCallback log.debug("""MSG OUT: %s""", message) for key, value in message.items(): self.sendLine('%s: %s' % (str(key.lower()), str(value))) self.sendLine('') return message['actionid'] def collectDeferred(self, message, stopEvent): """Collect all responses to this message until stopEvent or error returns deferred returning sequence of events/responses """ df = defer.Deferred() cache = [] def onEvent(event): if event.get('response') == 'Error': df.errback(error.AMICommandFailure(event)) elif event.get('event') == stopEvent: df.callback(cache) else: cache.append(event) actionid = self.sendMessage(message, onEvent) df.addCallbacks( self.cleanup, self.cleanup, callbackArgs=(actionid,), errbackArgs=(actionid,) ) return df def errorUnlessResponse(self, message, expected='Success'): """Raise AMICommandFailure error unless message['response'] == expected If == expected, returns the message """ if type(message) is dict and message['response'] != expected: raise error.AMICommandFailure(message) return message ## End-user API def absoluteTimeout(self, channel, timeout): """Set timeout value for the given channel (in seconds)""" message = { 'action': 'absolutetimeout', 'timeout': timeout, 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def agentLogoff(self, agent, soft): """Logs off the specified agent for the queue system.""" if soft in (True, 'yes', 1): soft = 'true' else: soft = 'false' message = { 'Action': 'AgentLogoff', 'Agent': agent, 'Soft': soft } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def agents(self): """Retrieve agents information""" message = { "action": "agents" } return self.collectDeferred(message, "AgentsComplete") def changeMonitor(self, channel, filename): """Change the file to which the channel is to be recorded""" message = { 'action': 'changemonitor', 'channel': channel, 'filename': filename } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def command(self, command): """Run asterisk CLI command, return deferred result for list of lines returns deferred returning list of lines (strings) of the command output. See listCommands to see available commands """ message = { 'action': 'command', 'command': command } df = self.sendDeferred(message) df.addCallback(self.errorUnlessResponse, expected='Follows') def onResult(message): 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): """Get extension state This command reports the extension state for the given extension. If the extension has a hint, this will report the status of the device connected to the extension. The following are the possible extension states: -2 Extension removed -1 Extension hint not found 0 Idle 1 In use 2 Busy""" message = { 'Action': 'ExtensionState', 'Exten': exten, 'Context': context } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def getConfig(self, filename): """Retrieves the data from an Asterisk configuration file""" message = { 'Action': 'GetConfig', 'filename': filename } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def getVar(self, channel, variable): """Retrieve the given variable from the channel""" def extractVariable(message): """When message comes in, extract the variable from it""" if variable.lower() in message: value = message[variable.lower()] elif 'value' in message: value = message['value'] else: raise error.AMICommandFailure(message) if value == '(null)': value = None return value message = { 'action': 'getvar', '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 'timeout' in message: 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 The results come in via multi-event callback channel -- channel name or None to retrieve all channels returns deferred returning list of Status Events for each requested channel """ message = { 'action': 'Status' } if channel: message['channel'] = channel return self.collectDeferred(message, 'StatusComplete') def stopMonitor(self, channel): """Stop monitoring the given channel""" message = { 'action': 'monitor', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def unpauseMonitor(self, channel): """Resume recording a channel""" message = { 'action': 'unpausemonitor', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def updateConfig(self, srcfile, dstfile, reload, headers={}): """Update a configuration file headers should be a dictionary with the following keys Action-XXXXXX Cat-XXXXXX Var-XXXXXX Value-XXXXXX Match-XXXXXX """ message = {} if reload in (True, 'yes', 1): reload = 'yes' else: reload = 'no' message = { 'action': 'updateconfig', 'srcfilename': srcfile, 'dstfilename': dstfile, 'reload': reload } for k, v in headers.items(): message[k] = v return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def userEvent(self, event, **headers): """Sends an arbitrary event to the Asterisk Manager Interface.""" message = { 'Action': 'UserEvent', 'userevent': event } for i, j in headers.items(): message[i] = j return self.sendMessage(message) def waitEvent(self, timeout): """Waits for an event to occur After calling this action, Asterisk will send you a Success response as soon as another event is queued by the AMI """ message = { 'action': 'WaitEvent', 'timeout': timeout } return self.collectDeferred(message, 'WaitEventComplete') def dahdiDNDoff(self, channel): """Toggles the DND state on the specified DAHDI channel to off""" messge = { 'action': 'DAHDIDNDoff', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiDNDon(self, channel): """Toggles the DND state on the specified DAHDI channel to on""" messge = { 'action': 'DAHDIDNDon', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiDialOffhook(self, channel, number): """Dial a number on a DAHDI channel while off-hook""" message = { 'Action': 'DAHDIDialOffhook', 'DAHDIChannel': channel, 'Number': number } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiHangup(self, channel): """Hangs up the specified DAHDI channel""" message = { 'Action': 'DAHDIHangup', 'DAHDIChannel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiRestart(self, channel): """Restarts the DAHDI channels, terminating any calls in progress""" message = { 'Action': 'DAHDIRestart', 'DAHDIChannel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) def dahdiShowChannels(self): """List all DAHDI channels""" message = { 'action': 'DAHDIShowChannels' } return self.collectDeferred(message, 'DAHDIShowChannelsComplete') def dahdiTransfer(self, channel): """Transfers DAHDI channel""" message = { 'Action': 'DAHDITransfer', 'channel': channel } return self.sendDeferred(message).addCallback(self.errorUnlessResponse) class AMIFactory(protocol.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 and return protocol instance Connect and return our (singleton) protocol instance with login completed. XXX This is messy, we'd much rather have the factory able to create large numbers of protocols simultaneously """ self.loginDefer = defer.Deferred() reactor.connectTCP(ip, port, self, timeout=timeout) return self.loginDefer def clientConnectionFailed(self, connector, reason): """Connection failed, report to our callers""" self.loginDefer.errback(reason) starpy-1.0.1/setup.py000077500000000000000000000075671171675153000145750ustar00rootroot00000000000000#!/usr/bin/env python # # StarPy -- Asterisk Protocols for Twisted # # Copyright (c) 2006, Michael C. Fletcher # # Michael C. Fletcher # # See http://asterisk-org.github.com/starpy/ for more information about the # StarPy project. Please do not directly contact any of the maintainers of this # project for assistance; the project provides a web site, mailing lists and # IRC channels for your use. # # This program is free software, distributed under the terms of the # BSD 3-Clause License. See the LICENSE file at the top of the source tree for # details. """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') 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.1', url = "http://asterisk-org.github.com/starpy/", description = "Twisted Protocols for interaction with the Asterisk PBX", author = "Mike C. Fletcher", author_email = "mcfletch@vrplumber.com", license = "BSD", package_dir = { 'starpy' : '.', }, packages = [ 'starpy', ], options = { 'sdist' : { 'force_manifest' : 1, 'formats' : ['gztar', 'zip'], }, }, data_files = dataFiles, cmdclass = { 'install_data' : smart_install_data }, **extraArguments )