├── asterisk ├── etc │ ├── extensions.ael.sample │ ├── extensions.conf.sample │ ├── pubsub.conf │ ├── xmpp.conf.sample │ ├── extensions_poc.conf │ └── extensions_ivrpoc.ael └── agi-bin │ ├── db.conf.sample │ ├── getjid.agi │ ├── getCustomer.agi │ ├── queueMembersCount.agi │ ├── queueMembers.agi │ ├── logonQueue.agi │ ├── logoffQueue.agi │ ├── createPubSubNode.agi │ └── deletePubSubNode.agi ├── audios ├── bye.wav ├── ivr.wav ├── logoff.wav ├── logon.wav ├── welcome.wav ├── try_again.wav ├── inform_code.wav ├── invalid_code.wav ├── ivr_nooption.wav ├── ivr_option1.wav ├── ivr_option2.wav ├── logon_failed.wav ├── support_team.wav ├── invalid_option.wav ├── logoff_failed.wav └── wait_a_moment.wav ├── .gitignore ├── software ├── software.conf.sample └── software.py ├── docs ├── ToDo ├── Requeriments ├── Requerimentos ├── Q&A ├── Q&A_ptbr ├── Install ├── Install_ptbr ├── Readme └── Readme_ptbr ├── queue ├── queueapp.conf.sample └── queueapp.py ├── README.md ├── db ├── initdata.sql └── dbpgsql.sql ├── tests ├── pubsub_events.py └── pubsub_client.py └── LICENSE /asterisk/etc/extensions.ael.sample: -------------------------------------------------------------------------------- 1 | #include "/etc/asterisk/extensions_ivrpoc.ael" 2 | 3 | -------------------------------------------------------------------------------- /audios/bye.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/bye.wav -------------------------------------------------------------------------------- /audios/ivr.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/ivr.wav -------------------------------------------------------------------------------- /audios/logoff.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/logoff.wav -------------------------------------------------------------------------------- /audios/logon.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/logon.wav -------------------------------------------------------------------------------- /audios/welcome.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/welcome.wav -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | software/software.conf 2 | queue/queueapp.conf 3 | asterisk/agi-bin/db.conf 4 | *.pyc 5 | -------------------------------------------------------------------------------- /audios/try_again.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/try_again.wav -------------------------------------------------------------------------------- /audios/inform_code.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/inform_code.wav -------------------------------------------------------------------------------- /audios/invalid_code.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/invalid_code.wav -------------------------------------------------------------------------------- /audios/ivr_nooption.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/ivr_nooption.wav -------------------------------------------------------------------------------- /audios/ivr_option1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/ivr_option1.wav -------------------------------------------------------------------------------- /audios/ivr_option2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/ivr_option2.wav -------------------------------------------------------------------------------- /audios/logon_failed.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/logon_failed.wav -------------------------------------------------------------------------------- /audios/support_team.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/support_team.wav -------------------------------------------------------------------------------- /audios/invalid_option.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/invalid_option.wav -------------------------------------------------------------------------------- /audios/logoff_failed.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/logoff_failed.wav -------------------------------------------------------------------------------- /audios/wait_a_moment.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhterres/IVR-Data-Delivery/HEAD/audios/wait_a_moment.wav -------------------------------------------------------------------------------- /asterisk/agi-bin/db.conf.sample: -------------------------------------------------------------------------------- 1 | [general] 2 | host=yourPostgreSQLServer 3 | name=dbname 4 | user=dbuser 5 | secret=dbsecret 6 | -------------------------------------------------------------------------------- /asterisk/etc/extensions.conf.sample: -------------------------------------------------------------------------------- 1 | #include "/etc/asterisk/extensions_poc.conf" 2 | 3 | [global] 4 | 5 | XMPPDOMAIN_POC=yourJabberDomain 6 | 7 | -------------------------------------------------------------------------------- /asterisk/etc/pubsub.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | xmppdomain=yourJabberDomain 3 | pubsubdomain=yourPubSubDomain 4 | xmppuser=yourAsteriskXmppAccount 5 | xmppsecret=yourAsteriskXmppAccountSecret 6 | 7 | -------------------------------------------------------------------------------- /software/software.conf.sample: -------------------------------------------------------------------------------- 1 | [general] 2 | xmppUser=user@yourJabberDomain/extennumber // Asterisk xmpp user 3 | xmppSecret=secret 4 | pubsubDomain=yourPubSubDomain 5 | nodeName=yourNodeName 6 | -------------------------------------------------------------------------------- /docs/ToDo: -------------------------------------------------------------------------------- 1 | queueapp.py 2 | ########### 3 | * implement more queue strategies (rrmemory) 4 | * verify device state when selecting destiny extension of call 5 | * play sound when queue has no members (hangup after sound?) 6 | * internal queue flow 7 | * investigate errors on channel hangup (more info in /tmp/queueapp.log) 8 | -------------------------------------------------------------------------------- /asterisk/etc/xmpp.conf.sample: -------------------------------------------------------------------------------- 1 | [xmppserver] 2 | type=client 3 | serverhost=yourJabberServer 4 | username=asterisk@yourJabberDomain 5 | secret=secret 6 | priority=1 7 | usetls=yes 8 | port=5222 9 | usesasl=yes 10 | status=available 11 | statusmessage="Asterisk Server" 12 | sendtodialplan=yes 13 | context=from_xmpp 14 | keepalive=yes 15 | 16 | -------------------------------------------------------------------------------- /queue/queueapp.conf.sample: -------------------------------------------------------------------------------- 1 | [queue] 2 | id=1 3 | name=support 4 | strategy=leastrecent 5 | timeout=60 6 | ringtimeperexten=15 7 | extension=0000 8 | 9 | [xmpp] 10 | xmppUser=user@yourJabberDomain/queue // Asterisk xmpp user 11 | xmppSecret=secret 12 | pubsubDomain=yourPubSubDomain 13 | 14 | [ari] 15 | user=yourAriUser 16 | secret=yourAriSecret 17 | 18 | [db] 19 | name=ivrdatadelivery 20 | host=yourPostgreSQLServer 21 | user=ivrdatadelivery 22 | secret=secret 23 | -------------------------------------------------------------------------------- /docs/Requeriments: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | * XMPP server with pubsub support (tested with ejabberd) 4 | * XMPP SRV DNS entries configured for your jabberdomain 5 | * Asterisk 13 with ARI enabled 6 | * Asterisk integrated with XMPP server (sample config file in asterisk/etc) 7 | * PostgreSQL server 8 | * sleekxmpp python library (https://github.com/fritzy/SleekXMPP) 9 | * pyst/pyst2 python library (https://github.com/rdegges/pyst2) 10 | * psycopg2 python library 11 | * ari-py python library (https://github.com/asterisk/ari-py) 12 | -------------------------------------------------------------------------------- /docs/Requerimentos: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | * Servidor XMPP com suporte pubsub (testado com ejabberd) 4 | * Entradas SRV para seu domínio jabber configuradas no seu DNS 5 | * Asterisk 13 com suporte a ARI ativado 6 | * Asterisk integrado com o servidor XMPP (arquivo de config de exemplo em asterisk/etc) 7 | * Servidor PostgreSQL 8 | * biblioteca python sleekxmpp instalada (https://github.com/fritzy/SleekXMPP) 9 | * biblioteca python pyst/pyst2 instalada (https://github.com/rdegges/pyst2) 10 | * biblioteca python psycopg2 instalada 11 | * biblioteca python ari-py instalada (https://github.com/asterisk/ari-py) 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IVR Data Delivery 2 | 3 | Delivering Asterisk IVR data to softwares using XMPP - Proof of Concept 4 | 5 | This project (PoC) shows the possibilities of integration between Asterisk IVRs and all kind of softwares using XMPP (Pubsub XEP-0060 - http://www.xmpp.org/extensions/xep-0060.html) 6 | 7 | I started this project to integrate Asterisk IVRs with customer service softwares, but, in fact, this is a multipurpose project that can be used with any kind of software that you want. 8 | 9 | This PoC is divided as follows: 10 | 11 | * Asterisk Stasis App (queueapp.py) 12 | * "Customer Service Software" - simulates a customer service software (software.py) 13 | * agi scripts 14 | * IVR (AEL diaplan) 15 | 16 | The directories contains: 17 | 18 | * asterisk/agi-bin - agi scripts 19 | * asterisk/etc - asterisk configuration files and dialplans 20 | * audios - audio files 21 | * db - database schema and initial daa (pgsql) 22 | * docs - documentation 23 | * queue - stasis queue application 24 | * software - "customer service software" 25 | * tests - scripts for testing 26 | 27 | If you find bugs, please send e-mail to bugs@mundoopensource.com.br. 28 | -------------------------------------------------------------------------------- /db/initdata.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 9.5.2 6 | -- Dumped by pg_dump version 9.5.2 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET client_encoding = 'UTF8'; 11 | SET standard_conforming_strings = on; 12 | SET check_function_bodies = false; 13 | SET client_min_messages = warning; 14 | SET row_security = off; 15 | 16 | SET search_path = public, pg_catalog; 17 | 18 | -- 19 | -- Data for Name: customer; Type: TABLE DATA; Schema: public; Owner: ivrdatadelivery 20 | -- 21 | 22 | COPY customer (id, name, code) FROM stdin; 23 | 1 Company 1 1111 24 | 2 Company 2 2222 25 | \. 26 | 27 | 28 | -- 29 | -- Name: ivrdatadelivery_id_customer; Type: SEQUENCE SET; Schema: public; Owner: ivrdatadelivery 30 | -- 31 | 32 | SELECT pg_catalog.setval('ivrdatadelivery_id_customer', 2, true); 33 | 34 | 35 | -- 36 | -- Name: ivrdatadelivery_id_queue; Type: SEQUENCE SET; Schema: public; Owner: ivrdatadelivery 37 | -- 38 | 39 | SELECT pg_catalog.setval('ivrdatadelivery_id_queue', 1, true); 40 | 41 | 42 | -- 43 | -- Data for Name: queue; Type: TABLE DATA; Schema: public; Owner: ivrdatadelivery 44 | -- 45 | 46 | COPY queue (id, name) FROM stdin; 47 | 1 support 48 | \. 49 | 50 | 51 | -- 52 | -- PostgreSQL database dump complete 53 | -- 54 | 55 | -------------------------------------------------------------------------------- /asterisk/etc/extensions_poc.conf: -------------------------------------------------------------------------------- 1 | [poc] 2 | 3 | ; ivr 4 | exten => 0000,1,NoOp(IVR PoC Support) 5 | same => n,goto(ivrpoc,ivr,1) 6 | 7 | ; ############ Logon 8 | exten => *56466*,1,NoOp(Logon queue support) 9 | same => n,Answer 10 | same => n,Playback(poc/wait_a_moment) 11 | same => n,Set(Logon=False) 12 | 13 | ; Pgsql 14 | same => n,agi(poc/logonQueue.agi,${CALLERID(num)},support) 15 | same => n,NoOp(Logon: ${Logon}.) 16 | same => n,GotoIf($["${Logon}" = "True"]?logon:error) 17 | same => n(error),Playback(poc/logon_failed) 18 | same => n,Playback(congestion) 19 | same => n,Hangup 20 | ; 21 | 22 | same => n(logon),agi(poc/createPubSubNode.agi,${CALLERID(num)}) 23 | ; AstDB 24 | same => n,Set(DB(support/extensions/${CALLERID(num)})=1) 25 | same => n,Playback(poc/logon) 26 | same => n,Playback(congestion) 27 | same => n,Hangup 28 | 29 | ; ############ Logoff 30 | exten => *564633*,1,NoOp(Logoff queue support) 31 | same => n,Answer 32 | same => n,Playback(poc/wait_a_moment) 33 | same => n,Set(Logoff=False) 34 | 35 | ; Pgsql 36 | same => n,agi(poc/logoffQueue.agi,${CALLERID(num)},support) 37 | same => n,NoOp(Logoff: ${Logoff}.) 38 | same => n,GotoIf($["${Logoff}" = "True"]?logoff:error) 39 | same => n(error),Playback(poc/logoff_failed) 40 | same => n,Playback(congestion) 41 | same => n,Hangup 42 | ; 43 | 44 | same => n(logoff),agi(poc/deletePubSubNode.agi,${CALLERID(num)}) 45 | ; AstDB 46 | same => n,DBDel(support/extensions/${CALLERID(num)}) 47 | same => n,Playback(poc/logoff) 48 | same => n,Playback(congestion) 49 | same => n,Hangup 50 | 51 | -------------------------------------------------------------------------------- /asterisk/agi-bin/getjid.agi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # getjid.py 5 | # Get JID of extension 6 | # 7 | # Marcelo H. Terres 8 | # 2016-09-09 9 | # 10 | 11 | import os 12 | import re 13 | import sys 14 | import psycopg2 15 | import psycopg2.extras 16 | import sys 17 | import ConfigParser 18 | 19 | from asterisk import agi 20 | 21 | 22 | try: 23 | 24 | extension=sys.argv[1] 25 | except: 26 | 27 | print "You need to inform extension..." 28 | sys.exit(1) 29 | 30 | configFile='%s/db.conf' % os.path.dirname(sys.argv[0]) 31 | 32 | try: 33 | 34 | configuration = ConfigParser.RawConfigParser() 35 | configuration.read(configFile) 36 | 37 | db_host=configuration.get('general','host') 38 | db_name=configuration.get('general','name') 39 | db_user=configuration.get('general','user') 40 | db_secret=configuration.get('general','secret') 41 | 42 | except: 43 | 44 | print "Config file %s not found." % configFile 45 | sys.exit(1) 46 | 47 | dsn = 'dbname=%s host=%s user=%s password=%s' % (db_name, db_host, db_user,db_secret) 48 | 49 | conn = psycopg2.connect(dsn) 50 | curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 51 | 52 | agi = agi.AGI() 53 | 54 | sql = "SELECT jid FROM sip WHERE extension = '%s';" % extension 55 | curs.execute(sql) 56 | 57 | if not curs.rowcount: 58 | agi.verbose("Can't find jid for extension %s" % extension) 59 | agi.set_variable("jid", "") 60 | sys.exit(1) 61 | 62 | jid = curs.fetchone()['jid'] 63 | agi.verbose('Jid %s' % jid) 64 | agi.set_variable('jid',jid) 65 | 66 | -------------------------------------------------------------------------------- /asterisk/agi-bin/getCustomer.agi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # getCustomer.py 5 | # Get Customer based on informed code 6 | # 7 | # Marcelo H. Terres 8 | # 2016-09-12 9 | # 10 | 11 | import os 12 | import re 13 | import sys 14 | import psycopg2 15 | import psycopg2.extras 16 | import sys 17 | import ConfigParser 18 | 19 | from asterisk import agi 20 | 21 | 22 | try: 23 | 24 | code=sys.argv[1] 25 | except: 26 | 27 | print "You need to inform code..." 28 | sys.exit(1) 29 | 30 | configFile='%s/db.conf' % os.path.dirname(sys.argv[0]) 31 | 32 | try: 33 | 34 | configuration = ConfigParser.RawConfigParser() 35 | configuration.read(configFile) 36 | 37 | db_host=configuration.get('general','host') 38 | db_name=configuration.get('general','name') 39 | db_user=configuration.get('general','user') 40 | db_secret=configuration.get('general','secret') 41 | 42 | except: 43 | 44 | print "Config file %s not found." % configFile 45 | sys.exit(1) 46 | 47 | dsn = 'dbname=%s host=%s user=%s password=%s' % (db_name, db_host, db_user,db_secret) 48 | 49 | conn = psycopg2.connect(dsn) 50 | curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 51 | 52 | agi = agi.AGI() 53 | 54 | sql = "SELECT id,name FROM customer WHERE code = '%s';" % code 55 | curs.execute(sql) 56 | 57 | if not curs.rowcount: 58 | agi.verbose("Can't find customer for code %s" % code) 59 | agi.set_variable("CUSTOMER_ID", "") 60 | agi.set_variable("CUSTOMER_NAME", "") 61 | sysexit(1) 62 | 63 | row = curs.fetchone() 64 | agi.verbose('CUSTOMER_ID %s' % row['id']) 65 | agi.verbose('CUSTOMER_NAME %s' % row['name']) 66 | agi.set_variable('CUSTOMER_ID',row['id']) 67 | agi.set_variable('CUSTOMER_NAME',row['name']) 68 | 69 | -------------------------------------------------------------------------------- /asterisk/agi-bin/queueMembersCount.agi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # queueMembersCount 5 | # Get the number of members in custom queue support 6 | # 7 | # Marcelo H. Terres 8 | # 2016-09-11 9 | # 10 | 11 | import sys 12 | import os 13 | import re 14 | import sys 15 | import psycopg2 16 | import psycopg2.extras 17 | import sys 18 | import ConfigParser 19 | 20 | from asterisk import agi 21 | 22 | try: 23 | 24 | queue=sys.argv[1] 25 | except: 26 | 27 | print "You need to inform queue..." 28 | sys.exit(1) 29 | 30 | configFile='%s/db.conf' % os.path.dirname(sys.argv[0]) 31 | 32 | try: 33 | 34 | configuration = ConfigParser.RawConfigParser() 35 | configuration.read(configFile) 36 | 37 | db_host=configuration.get('general','host') 38 | db_name=configuration.get('general','name') 39 | db_user=configuration.get('general','user') 40 | db_secret=configuration.get('general','secret') 41 | 42 | except: 43 | 44 | print "Config file %s not found." % configFile 45 | sys.exit(1) 46 | 47 | 48 | agi = agi.AGI() 49 | 50 | dsn = 'dbname=%s host=%s user=%s password=%s' % (db_name, db_host, db_user,db_secret) 51 | 52 | conn = psycopg2.connect(dsn) 53 | curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 54 | 55 | sql = "SELECT id FROM queue WHERE name = '%s';" % queue 56 | curs.execute(sql) 57 | 58 | if not curs.rowcount: 59 | agi.verbose("Can't find queue %s" % queue) 60 | agi.set_variable("Logon", "False") 61 | sys.exit(1) 62 | 63 | queue_id = curs.fetchone()['id'] 64 | 65 | sql = "SELECT count(id) AS total FROM queue_sip WHERE queue_id = %s AND logged=True;" % queue_id 66 | curs.execute(sql) 67 | 68 | if not curs.rowcount: 69 | count=0 70 | else: 71 | 72 | count = curs.fetchone()['total'] 73 | 74 | conn.commit() 75 | conn.close() 76 | 77 | agi.set_variable("members_count", count) 78 | 79 | -------------------------------------------------------------------------------- /docs/Q&A: -------------------------------------------------------------------------------- 1 | Why do I need XMPP? 2 | ******************* 3 | Why don't you do a direct connection between Asterisk and the "other software"? 4 | ******************************************************************************* 5 | 6 | Answering both questions: the choice of using XMPP (PubSub) as a middleware between Asterisk and the customer service software has many reasons: 7 | 8 | * With this approach, the information exchange between Asterisk and the "other software" don't need to be modified to each case, making the solution as generic as possible. 9 | * For security reasons, the Asterisk server don't need to have direct access to the "other software's" infrastructure (and vice versa). 10 | * The "other software" can be hosted in another place or network that is not acessible to Asterisk (and vice versa). 11 | * XMPP (and PubSub) is a mature protocol and you can find XMPP libraries in many major languages (Python, Java, Perl, etc...). 12 | * Besides, with PubSub, you can implement the solution in your software with just a few lines of code (see software/software.py) and with little efforts. 13 | * All servers can be in diffent places or networks. You just need to ensure that Asterisk and the "other software" can communicate with the XMPP server. 14 | 15 | Do I need to use PostgreSQL? 16 | **************************** 17 | 18 | No, you can use your preferred DBMS (MySQL, MsSql, Oracle, SQLite, etc...). I just opted for PostgreSQL because I use it in a daily basis. But attention: if you decide to use a different DBMS you'll need to edit the agis and the scripts to connect in this new DBMS. 19 | 20 | What XMPP server should I use? 21 | ****************************** 22 | 23 | You can use any XMPP server that supports XEP-0060 (PubSub). I chose ejabberd, but you can use Openfire, MongooseIM, Tigase and Prosody, for example. 24 | 25 | What kind of software I can integrate with this solution? 26 | ******************************************************** 27 | 28 | You can integrate any software that can make use of informations obtained through IVRs, like customer service software, HelpDesk, CRM and ombudsman softwares. 29 | -------------------------------------------------------------------------------- /asterisk/agi-bin/queueMembers.agi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # queueMembers 5 | # Get members of custom queue support 6 | # 7 | # Marcelo H. Terres 8 | # 2016-09-11 9 | # 10 | 11 | import sys 12 | import os 13 | import re 14 | import sys 15 | import psycopg2 16 | import psycopg2.extras 17 | import sys 18 | import ConfigParser 19 | 20 | from asterisk import agi 21 | 22 | try: 23 | 24 | queue=sys.argv[1] 25 | except: 26 | 27 | print "You need to inform queue..." 28 | sys.exit(1) 29 | 30 | configFile='%s/db.conf' % os.path.dirname(sys.argv[0]) 31 | 32 | try: 33 | 34 | configuration = ConfigParser.RawConfigParser() 35 | configuration.read(configFile) 36 | 37 | db_host=configuration.get('general','host') 38 | db_name=configuration.get('general','name') 39 | db_user=configuration.get('general','user') 40 | db_secret=configuration.get('general','secret') 41 | 42 | except: 43 | 44 | print "Config file %s not found." % configFile 45 | sys.exit(1) 46 | 47 | 48 | agi = agi.AGI() 49 | 50 | dsn = 'dbname=%s host=%s user=%s password=%s' % (db_name, db_host, db_user,db_secret) 51 | 52 | conn = psycopg2.connect(dsn) 53 | curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 54 | 55 | sql = "SELECT id FROM queue WHERE name = '%s';" % queue 56 | curs.execute(sql) 57 | 58 | if not curs.rowcount: 59 | agi.verbose("Can't find queue %s" % queue) 60 | agi.set_variable("Logon", "False") 61 | sys.exit(1) 62 | 63 | queue_id = curs.fetchone()['id'] 64 | 65 | sql = "SELECT sip.extension AS exten FROM queue_sip,sip WHERE queue_id = %s AND sip.id=queue_sip.sip_id AND queue_sip.logged=True;" % queue_id 66 | curs.execute(sql) 67 | 68 | members="" 69 | 70 | if curs.rowcount>0: 71 | 72 | row = curs.fetchone() 73 | 74 | while row is not None: 75 | 76 | 77 | if len(members.strip())==0: 78 | 79 | members=row['exten'] 80 | else: 81 | members="%s,%s" % (members,row['exten']) 82 | 83 | row = curs.fetchone() 84 | 85 | conn.commit() 86 | conn.close() 87 | 88 | agi.set_variable("queue_members", members) 89 | 90 | -------------------------------------------------------------------------------- /docs/Q&A_ptbr: -------------------------------------------------------------------------------- 1 | Porque preciso usar o XMPP? 2 | *************************** 3 | Não posso fazer uma comunicação direta entre o Asterisk e a aplicação? 4 | ********************************************************************** 5 | 6 | Respondendo ambas perguntas: a escolha da utilização do XMPP (PubSub) como um intermediário entre o Asterisk e o software de atendimento ao cliente tem várias razões: 7 | 8 | * Com esta abordagem, a troca de informações entre o Asterisk e o software não precisa ser tratada caso a caso, fazendo com que a solução seja o mais genérica possível. 9 | * Por questões de segurança, o servidor Asterisk não precisa ter acesso ao software que roda a aplicação (e vice-versa). 10 | * A aplicação pode rodar em um local inacessível diretamente ao servidor Asterisk (e vice-versa). 11 | * O protocolo XMPP (e o PubSub) é muito maduro e possui bibliotecas disponíveis para todas as grandes linguagens, fazendo como que seu uso seja muito simples na aplicação. 12 | * Além disso, com o PubSub, a modificação necessária no software, para recebimento dos dados do Asterisk, pode ser efetuada com poucas linhas de código (vide software/software.py). 13 | * Todos servidores envolvidos podem estar em redes e locais distintos. Para que a solução funcione só é preciso que tanto o servidor de aplicação como o servidor Asterisk possam falar com o servidor XMPP, numa comunicação mínima de troca de mensagens. 14 | 15 | Preciso usar o PostgreSQL? 16 | ************************** 17 | 18 | Não, você pode utilizar o banco que lhe for mais conveniente. Optei pelo PostgreSQL pois é meu banco de trabalho padrão. Mas atenção: se você for usar outro DBMS (MySQL, Oracle, MSSQL, etc...), precisará alterar os agis e os scripts para conexão no mesmo. 19 | 20 | Qual XMPP server eu devo usar? 21 | ****************************** 22 | 23 | Você pode usar qualquer servidor XMPP que implemente a XEP-0060 (PubSub). Eu escolhi o ejabberd, mas você pode utilizar outro, como por exemplo Openfire, MongooseIM, Tigase e Prosody. 24 | 25 | Que tipo de software eu posso integrar com essa solução? 26 | ******************************************************** 27 | 28 | Você pode integrar qualquer tipo de software que possa fazer uso de informações obtidas através de uma URA. Caso clássicos são softwares de HelpDesk, SAC, CRM e Ouvidoria. 29 | -------------------------------------------------------------------------------- /asterisk/agi-bin/logonQueue.agi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # logonQueue.agi 5 | # JID logon in a custom queue 6 | # 7 | # Marcelo H. Terres 8 | # 2016-09-11 9 | # 10 | 11 | import os 12 | import re 13 | import sys 14 | import psycopg2 15 | import psycopg2.extras 16 | import sys 17 | import ConfigParser 18 | 19 | from asterisk import agi 20 | 21 | try: 22 | 23 | extension=sys.argv[1] 24 | except: 25 | 26 | print "You need to inform extension..." 27 | sys.exit(1) 28 | 29 | try: 30 | 31 | queue=sys.argv[2] 32 | except: 33 | 34 | print "You need to inform queue..." 35 | sys.exit(1) 36 | 37 | configFile='%s/db.conf' % os.path.dirname(sys.argv[0]) 38 | 39 | try: 40 | 41 | configuration = ConfigParser.RawConfigParser() 42 | configuration.read(configFile) 43 | 44 | db_host=configuration.get('general','host') 45 | db_name=configuration.get('general','name') 46 | db_user=configuration.get('general','user') 47 | db_secret=configuration.get('general','secret') 48 | 49 | except: 50 | 51 | print "Config file %s not found." % configFile 52 | sys.exit(1) 53 | 54 | dsn = 'dbname=%s host=%s user=%s password=%s' % (db_name, db_host, db_user,db_secret) 55 | 56 | conn = psycopg2.connect(dsn) 57 | curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 58 | 59 | agi = agi.AGI() 60 | 61 | sql = "SELECT id FROM queue WHERE name = '%s';" % queue 62 | curs.execute(sql) 63 | 64 | if not curs.rowcount: 65 | agi.verbose("Can't find queue %s" % queue) 66 | agi.set_variable("Logon", "False") 67 | sys.exit(1) 68 | 69 | queue_id = curs.fetchone()['id'] 70 | 71 | sql = "SELECT id FROM sip WHERE extension = '%s';" % extension 72 | curs.execute(sql) 73 | 74 | if not curs.rowcount: 75 | agi.verbose("Can't find extension %s" % extension) 76 | agi.set_variable("Logon", "False") 77 | sys.exit(1) 78 | 79 | sip_id = curs.fetchone()['id'] 80 | 81 | sql = "SELECT * FROM queue_sip WHERE queue_id=%s and sip_id=%s;" % (queue_id,sip_id) 82 | curs.execute(sql) 83 | 84 | if not curs.rowcount: 85 | sql="INSERT INTO queue_sip (queue_id,sip_id,logged) VALUES (%s,%s,True);" % (queue_id,sip_id) 86 | else: 87 | 88 | sql="UPDATE queue_sip SET logged=True WHERE queue_id=%s and sip_id=%s;" % (queue_id,sip_id) 89 | 90 | print sql 91 | 92 | curs.execute(sql) 93 | conn.commit() 94 | conn.close() 95 | 96 | agi.set_variable("Logon", "True") 97 | 98 | -------------------------------------------------------------------------------- /docs/Install: -------------------------------------------------------------------------------- 1 | Installation 2 | ************ 3 | 4 | This document supposes the following scenario: 5 | 6 | * All requirements are ok (docs/Requeriments) 7 | * Asterisk was compiled from source 8 | * Asterisk is using the english language 9 | 10 | If you have a different scenario, you will need to change some directories in following operations. 11 | 12 | Asterisk 13 | ******** 14 | 15 | Access the directory IVR-Data-Delivery and run these commands: 16 | 17 | cp -rp audios /var/lib/asterisk/sounds/en/poc 18 | cp -rp asterisk/agi-bin /var/lib/asterisk/agi-bin/poc 19 | cp asterisk/etc/extensions_poc.conf /etc/asterisk 20 | cp asterisk/etc/extensions_ivrpoc.ael /etc/asterisk 21 | cp asterisk/etc/pubsub.conf /etc/asterisk 22 | cp /var/lib/asterisk/agi-bin/poc/db.conf.sample /var/lib/asterisk/agi-bin/poc/db.conf 23 | cp software/software.conf.sample software/software.conf 24 | cp queue/queueapp.conf.sample queue/queueapp.conf 25 | cat asterisk/etc/extensions.ael.sample >> /etc/asterisk/extensions.ael 26 | 27 | Add in the beginning of file /etc/asterisk/extensions.conf this line: 28 | 29 | #include "/etc/asterisk/extensions_poc.conf" 30 | 31 | Add to context [global] in file /etc/asterisk/extensions.conf this line: 32 | 33 | XMPPDOMAIN_POC=yourJabberDomain 34 | 35 | Change the XMPPDOMAIN_POC value to your jabber domain. 36 | 37 | 38 | Add to your extensions context (file /etc/asterisk/extensions.conf) this line: 39 | 40 | include => poc 41 | 42 | XMPP 43 | **** 44 | 45 | Create xmpp accounts that will be linked with Asterisk extensions. 46 | 47 | PostgreSQL 48 | ********** 49 | 50 | Create an user and database with name ivrdatadelivery and import the .sql files in database. 51 | 52 | su - postgres 53 | createuser -P -S -D -R ivrdatadelivery 54 | createdb -O ivrdatadelivery ivrdatadelivery 55 | exit 56 | cat db/dbpgsql.sql | psql -U ivrdatadelivery -W ivrdatadelivery -h 127.0.0.1 57 | cat db/initdata.sql | psql -U ivrdatadelivery -W ivrdatadelivery -h 127.0.0.1 58 | 59 | Edit sip table and add the extensions. Don't forget to link with XMPP accounts. 60 | 61 | 62 | Final configurations 63 | ******************** 64 | 65 | Edit file /etc/asterisk/pubsub.conf and change the parameters as needed 66 | Edit file /var/lib/asterisk/agi-bin/poc/db.conf and change the parameters as needed 67 | Edit file software/software.conf and change the parameters as needed 68 | Edit file queue/queueapp.conf and change the parameters as needed 69 | 70 | 71 | -------------------------------------------------------------------------------- /asterisk/agi-bin/logoffQueue.agi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # logoffQueue.agi 5 | # JID logff in a custom queue 6 | # 7 | # Marcelo H. Terres 8 | # 2016-09-11 9 | # 10 | 11 | import os 12 | import re 13 | import sys 14 | import psycopg2 15 | import psycopg2.extras 16 | import sys 17 | import ConfigParser 18 | 19 | from asterisk import agi 20 | 21 | try: 22 | 23 | extension=sys.argv[1] 24 | except: 25 | 26 | print "You need to inform extension..." 27 | sys.exit(1) 28 | 29 | try: 30 | 31 | queue=sys.argv[2] 32 | except: 33 | 34 | print "You need to inform queue..." 35 | sys.exit(1) 36 | 37 | configFile='%s/db.conf' % os.path.dirname(sys.argv[0]) 38 | 39 | try: 40 | 41 | configuration = ConfigParser.RawConfigParser() 42 | configuration.read(configFile) 43 | 44 | db_host=configuration.get('general','host') 45 | db_name=configuration.get('general','name') 46 | db_user=configuration.get('general','user') 47 | db_secret=configuration.get('general','secret') 48 | 49 | except: 50 | 51 | print "Config file %s not found." % configFile 52 | sys.exit(1) 53 | 54 | dsn = 'dbname=%s host=%s user=%s password=%s' % (db_name, db_host, db_user,db_secret) 55 | 56 | conn = psycopg2.connect(dsn) 57 | curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 58 | 59 | agi = agi.AGI() 60 | 61 | sql = "SELECT id FROM queue WHERE name = '%s';" % queue 62 | curs.execute(sql) 63 | 64 | if not curs.rowcount: 65 | agi.verbose("Can't find queue %s" % queue) 66 | agi.set_variable("Logoff", "False") 67 | sys.exit(1) 68 | 69 | queue_id = curs.fetchone()['id'] 70 | 71 | sql = "SELECT id FROM sip WHERE extension = '%s';" % extension 72 | curs.execute(sql) 73 | 74 | if not curs.rowcount: 75 | agi.verbose("Can't find extension %s" % extension) 76 | agi.set_variable("Logoff", "False") 77 | sys.exit(1) 78 | 79 | sip_id = curs.fetchone()['id'] 80 | 81 | sql = "SELECT * FROM queue_sip WHERE queue_id=%s and sip_id=%s;" % (queue_id,sip_id) 82 | curs.execute(sql) 83 | 84 | if not curs.rowcount: 85 | agi.verbose("Can't find extension %s logged on queue %s" % (extension,queue)) 86 | agi.set_variable("Logoff", "False") 87 | sys.exit(1) 88 | else: 89 | 90 | sql="UPDATE queue_sip SET logged=False WHERE queue_id=%s and sip_id=%s;" % (queue_id,sip_id) 91 | 92 | curs.execute(sql) 93 | conn.commit() 94 | conn.close() 95 | 96 | agi.set_variable("Logoff", "True") 97 | 98 | -------------------------------------------------------------------------------- /docs/Install_ptbr: -------------------------------------------------------------------------------- 1 | Instalação da PoC 2 | ***************** 3 | 4 | Este documento supõe que exista o seguinte cenário: 5 | 6 | * Os requerimentos indicados (docs/Requerimentos) estejam sendo atendidos 7 | * O Asterisk tenha sido compilado dos fontes 8 | * O Asterisk esteja configurado para uso da linguagem pt_br 9 | 10 | Se o cenário for diferente, você precisará alterar alguns diretórios nos comandos seguintes 11 | 12 | Asterisk 13 | ******** 14 | 15 | Acesse o diretório IVR-Data-Delivery e rode os seguintes comandos: 16 | 17 | cp -rp audios /var/lib/asterisk/sounds/pt_BR/poc 18 | cp -rp asterisk/agi-bin /var/lib/asterisk/agi-bin/poc 19 | cp asterisk/etc/extensions_poc.conf /etc/asterisk 20 | cp asterisk/etc/extensions_ivrpoc.ael /etc/asterisk 21 | cp asterisk/etc/pubsub.conf /etc/asterisk 22 | cp /var/lib/asterisk/agi-bin/poc/db.conf.sample /var/lib/asterisk/agi-bin/poc/db.conf 23 | cp software/software.conf.sample software/software.conf 24 | cp queue/queueapp.conf.sample queue/queueapp.conf 25 | cat asterisk/etc/extensions.ael.sample >> /etc/asterisk/extensions.ael 26 | 27 | Adicione ao início do arquivo /etc/asterisk/extensions.conf a seguinte linha: 28 | 29 | #include "/etc/asterisk/extensions_poc.conf" 30 | 31 | Adicione ao contexto [global] do arquivo /etc/asterisk/extensions.conf a seguinte linha: 32 | 33 | XMPPDOMAIN_POC=yourJabberDomain 34 | 35 | Altere o XMPPDOMAIN_POC para seu domínio jabber. 36 | 37 | Adicione ao contexto de seus ramais (arquivo /etc/asterisk/extensions.conf) a seguinte linha: 38 | 39 | include => poc 40 | 41 | XMPP 42 | **** 43 | 44 | Crie os usuários que serão vinculados aos ramais de teste. 45 | 46 | PostgreSQL 47 | ********** 48 | Crie um usuário e um banco de dados com o nome ivrdatadelivery e carregue os arquivos .sql existentes no diretório db. 49 | 50 | su - postgres 51 | createuser -P -S -D -R ivrdatadelivery 52 | createdb -O ivrdatadelivery ivrdatadelivery 53 | exit 54 | cat db/dbpgsql.sql | psql -U ivrdatadelivery -W ivrdatadelivery -h 127.0.0.1 55 | cat db/initdata.sql | psql -U ivrdatadelivery -W ivrdatadelivery -h 127.0.0.1 56 | 57 | Edite a tabela sip e adicione os ramais necessários vinculados as contas XMPP. 58 | 59 | Configurações finais 60 | ******************** 61 | 62 | Edite o arquivo /etc/asterisk/pubsub.conf e altere os dados necessários 63 | Edite o arquivo /var/lib/asterisk/agi-bin/poc/db.conf e altere os dados necessários 64 | Edite o arquivo software/software.conf e altere os dados necessários 65 | Edite o arquivo queue/queueapp.conf e altere os dados necessários 66 | 67 | -------------------------------------------------------------------------------- /asterisk/etc/extensions_ivrpoc.ael: -------------------------------------------------------------------------------- 1 | context ivrpoc { 2 | 3 | s => { 4 | 5 | goto ivrpoc|ivr|background; 6 | }; 7 | 8 | ivr => { 9 | 10 | Set(QUEUENAME=support); 11 | x=0; 12 | 13 | Answer; 14 | Wait(1); 15 | 16 | background: 17 | Background(poc/welcome); 18 | 19 | backgroundoptions: 20 | 21 | Background(poc/ivr); 22 | WaitExten(3); 23 | 24 | Dial(PJSIP/3000,50,tT); 25 | }; 26 | 27 | 1 => { 28 | // Support 29 | goto ivrsupport|support|1; 30 | }; 31 | 32 | 2 => { 33 | // Sales 34 | Dial(PJSIP/2000,50,tT); 35 | }; 36 | 37 | i => { 38 | 39 | x=${x}+1; 40 | Playback(poc/invalid_option); 41 | goto ivrpoc|ivr|backgroundoptions; 42 | }; 43 | }; 44 | 45 | context ivrsupport { 46 | 47 | s => { 48 | 49 | goto ivrsupport|support|1; 50 | }; 51 | 52 | support => { 53 | 54 | NoOp(Support selected); 55 | 56 | code: 57 | y=0; 58 | 59 | entercode: 60 | y=${y}+1; 61 | Read(code,poc/inform_code,4); 62 | 63 | NoOp(Code ${code}); 64 | 65 | agi(poc/getCustomer.agi,${code}); 66 | 67 | NoOp("${CUSTOMER_ID}"); 68 | 69 | if ("${CUSTOMER_ID}" = "") 70 | { 71 | if (${y}=3) 72 | { 73 | wait(1); 74 | Playback(poc/invalid_code); 75 | Playback(poc/bye); 76 | Playback(congestion); 77 | Hangup; 78 | } 79 | 80 | wait(1); 81 | Playback(poc/invalid_code); 82 | Playback(poc/try_again); 83 | goto ivrsupport|support|entercode; 84 | } 85 | else 86 | { 87 | goto support|${CUSTOMER_NAME}|1; 88 | } 89 | }; 90 | }; 91 | 92 | context support { 93 | 94 | h => { 95 | Hangup; 96 | } 97 | 98 | _. => { 99 | NoOP (Customer ${CUSTOMER_NAME}); 100 | 101 | Set(JABBERMSG="Support: Customer ${CUSTOMER_NAME}"); 102 | 103 | &XMPPSupportMSG(${JABBERMSG}); 104 | 105 | wait(1); 106 | Stasis(queueapp,${CUSTOMER_ID}|${CUSTOMER_NAME}); 107 | }; 108 | }; 109 | 110 | context macro-XMPPSupportMSG { 111 | 112 | s => { 113 | 114 | &XMPPSupportMSG(${ARG1}); 115 | }; 116 | }; 117 | 118 | macro XMPPSupportMSG(JABBERMSG) { 119 | 120 | x=0; 121 | 122 | agi(poc/queueMembersCount.agi,${QUEUENAME}); // return number of queue active members in members_count variable 123 | 124 | if (${members_count} > 0) { 125 | 126 | agi(poc/queueMembers.agi,${QUEUENAME}); // return active members of queue in queue_members variable 127 | 128 | NoOP(${members_count} members in queue support - ${queue_members}); 129 | 130 | while (${x} < ${members_count}) { 131 | 132 | x = ${x} + 1; 133 | 134 | if (${members_count} = 1) { 135 | 136 | Set(member=${queue_members}); 137 | } 138 | else { 139 | 140 | Set(member=${CUT(queue_members,\,,${x})}); 141 | } 142 | 143 | agi(poc/getjid.agi,${member}); // get jid of extension in jid variable 144 | Set(user=${CUT(jid,"@",1)}); 145 | NoOP(Sending message to extension ${member} - jabber id ${jid}); 146 | &SendJabberIvr(${user},${JABBERMSG}); 147 | } 148 | } 149 | }; 150 | 151 | context macro-SendJabberIvr { 152 | 153 | s => { 154 | 155 | &SendJabberIvr(${ARG1},${ARG2}); 156 | }; 157 | }; 158 | 159 | macro SendJabberIvr(DST, MSG) { 160 | 161 | NoOp (Sending message to ${DST}); 162 | 163 | JabberSend(xmppserver,${DST}@${XMPPDOMAIN_POC},${MSG}); 164 | }; 165 | 166 | -------------------------------------------------------------------------------- /software/software.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # software.py 5 | # "customer service software" 6 | # For pubsub testing 7 | # 8 | # Marcelo H. Terres 9 | # 2016-04-10 10 | # 11 | # Based on pubsub_events.py 12 | 13 | import os 14 | import sys 15 | import sleekxmpp 16 | import ConfigParser 17 | import xml.etree.cElementTree as ET 18 | 19 | from sleekxmpp.xmlstream import ET, tostring 20 | from sleekxmpp.xmlstream.matcher import StanzaPath 21 | from sleekxmpp.xmlstream.handler import Callback 22 | 23 | # Python versions before 3.0 do not use UTF-8 encoding 24 | # by default. To ensure that Unicode is handled properly 25 | # throughout SleekXMPP, we will set the default encoding 26 | # ourselves to UTF-8. 27 | if sys.version_info < (3, 0): 28 | from sleekxmpp.util.misc_ops import setdefaultencoding 29 | setdefaultencoding('utf8') 30 | else: 31 | raw_input = input 32 | 33 | configFile='%s/software.conf' % os.path.dirname(sys.argv[0]) 34 | 35 | IDs=[] 36 | 37 | try: 38 | 39 | configuration = ConfigParser.RawConfigParser() 40 | configuration.read(configFile) 41 | 42 | # xmpp config 43 | 44 | nodeName=configuration.get('general','nodeName') 45 | pubsubDomain=configuration.get('general','pubsubDomain') 46 | xmppUser=configuration.get('general','xmppUser') 47 | xmppSecret=configuration.get('general','xmppSecret') 48 | 49 | except: 50 | 51 | print "Config file %s not found." % configFile 52 | sys.exit(1) 53 | 54 | class SW_XMPP(sleekxmpp.ClientXMPP): 55 | 56 | def __init__(self, jid, password): 57 | super(SW_XMPP, self).__init__(jid, password) 58 | 59 | self.register_plugin('xep_0030') 60 | self.register_plugin('xep_0059') 61 | self.register_plugin('xep_0060') 62 | 63 | self.register_handler( 64 | Callback('Pubsub event', 65 | StanzaPath('message/pubsub_event'), 66 | self._handle_event)) 67 | 68 | self.add_event_handler('session_start', self.start) 69 | 70 | def start(self, event): 71 | self.get_roster() 72 | self.send_presence() 73 | 74 | print('started connection on %s' % pubsubDomain) 75 | node = nodeName 76 | try: 77 | self['xep_0060'].subscribe(pubsubDomain,nodeName) 78 | print('subscribed to %s on %s' % (nodeName,pubsubDomain)) 79 | except: 80 | print('failed to subscribe to %s on %s' % (nodeName,pubsubDomain)) 81 | self.disconnect() 82 | 83 | def _handle_event(self, msg): 84 | data='''%s''' % msg['pubsub_event'] 85 | tree=ET.fromstring(data) 86 | 87 | # Avoid display the same item more than once 88 | # It started to happens and I don't discover why yet 89 | # Maybe a XMPP server PubSub implementation bug? 90 | # Needs more investigation (Try Tigase and Prosody PubSub) 91 | id=tree[0][0].get("id") 92 | 93 | if id in IDs: 94 | 95 | idx=1 96 | else: 97 | idx=0 98 | IDs.append(id) 99 | 100 | if idx==0: 101 | 102 | #print "pubsub Item: %s" % data 103 | print "Data received: %s" % tree[0][0][0].text 104 | try: 105 | result = self['xep_0060'].purge(pubsubDomain, nodeName) 106 | #print('Purged all items from node %s on %s' % (nodeName,pubsubDomain)) 107 | except: 108 | print('Could not purge items from node %s on %s' % (nodeName,pubsubDomain) ) 109 | 110 | print "Connecting as %s" % xmppUser 111 | 112 | xmpp = SW_XMPP(xmppUser,xmppSecret) 113 | 114 | if xmpp.connect(): 115 | xmpp.process(block=True) 116 | else: 117 | print 'Unable to connect' 118 | 119 | -------------------------------------------------------------------------------- /tests/pubsub_events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import logging 6 | import getpass 7 | from optparse import OptionParser 8 | 9 | import sleekxmpp 10 | from sleekxmpp.xmlstream import ET, tostring 11 | from sleekxmpp.xmlstream.matcher import StanzaPath 12 | from sleekxmpp.xmlstream.handler import Callback 13 | 14 | 15 | # Python versions before 3.0 do not use UTF-8 encoding 16 | # by default. To ensure that Unicode is handled properly 17 | # throughout SleekXMPP, we will set the default encoding 18 | # ourselves to UTF-8. 19 | if sys.version_info < (3, 0): 20 | reload(sys) 21 | sys.setdefaultencoding('utf8') 22 | else: 23 | raw_input = input 24 | 25 | 26 | class PubsubEvents(sleekxmpp.ClientXMPP): 27 | 28 | def __init__(self, jid, password): 29 | super(PubsubEvents, self).__init__(jid, password) 30 | 31 | self.register_plugin('xep_0030') 32 | self.register_plugin('xep_0059') 33 | self.register_plugin('xep_0060') 34 | 35 | self.register_handler( 36 | Callback('Pubsub event', 37 | StanzaPath('message/pubsub_event'), 38 | self._handle_event)) 39 | 40 | self.add_event_handler('session_start', self.start) 41 | 42 | def start(self, event): 43 | self.get_roster() 44 | self.send_presence() 45 | 46 | def _handle_event(self, msg): 47 | print msg 48 | print "" 49 | print "" 50 | print('Received pubsub event: %s' % msg['pubsub_event']) 51 | 52 | 53 | 54 | if __name__ == '__main__': 55 | # Setup the command line arguments. 56 | optp = OptionParser() 57 | 58 | # Output verbosity options. 59 | optp.add_option('-q', '--quiet', help='set logging to ERROR', 60 | action='store_const', dest='loglevel', 61 | const=logging.ERROR, default=logging.INFO) 62 | optp.add_option('-d', '--debug', help='set logging to DEBUG', 63 | action='store_const', dest='loglevel', 64 | const=logging.DEBUG, default=logging.INFO) 65 | optp.add_option('-v', '--verbose', help='set logging to COMM', 66 | action='store_const', dest='loglevel', 67 | const=5, default=logging.INFO) 68 | 69 | # JID and password options. 70 | optp.add_option("-j", "--jid", dest="jid", 71 | help="JID to use") 72 | optp.add_option("-p", "--password", dest="password", 73 | help="password to use") 74 | 75 | opts, args = optp.parse_args() 76 | 77 | # Setup logging. 78 | logging.basicConfig(level=opts.loglevel, 79 | format='%(levelname)-8s %(message)s') 80 | 81 | if opts.jid is None: 82 | opts.jid = raw_input("Username: ") 83 | if opts.password is None: 84 | opts.password = getpass.getpass("Password: ") 85 | 86 | # Setup the PubsubEvents listener 87 | xmpp = PubsubEvents(opts.jid, opts.password) 88 | 89 | # If you are working with an OpenFire server, you may need 90 | # to adjust the SSL version used: 91 | # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 92 | 93 | # If you want to verify the SSL certificates offered by a server: 94 | # xmpp.ca_certs = "path/to/ca/cert" 95 | 96 | # Connect to the XMPP server and start processing XMPP stanzas. 97 | if xmpp.connect(): 98 | # If you do not have the dnspython library installed, you will need 99 | # to manually specify the name of the server if it does not match 100 | # the one in the JID. For example, to use Google Talk you would 101 | # need to use: 102 | # 103 | # if xmpp.connect(('talk.google.com', 5222)): 104 | # ... 105 | xmpp.process(block=True) 106 | print("Done") 107 | else: 108 | print("Unable to connect.") 109 | -------------------------------------------------------------------------------- /asterisk/agi-bin/createPubSubNode.agi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # createPubSubNode.py 5 | # Create PubSub Node of extension 6 | # 7 | # Marcelo H. Terres 8 | # 2016-04-10 9 | # 10 | # Based on pubsub_client.py from sleekXMPP project 11 | 12 | import sys 13 | import sleekxmpp 14 | import ConfigParser 15 | 16 | # Python versions before 3.0 do not use UTF-8 encoding 17 | # by default. To ensure that Unicode is handled properly 18 | # throughout SleekXMPP, we will set the default encoding 19 | # ourselves to UTF-8. 20 | if sys.version_info < (3, 0): 21 | from sleekxmpp.util.misc_ops import setdefaultencoding 22 | setdefaultencoding('utf8') 23 | else: 24 | raw_input = input 25 | 26 | class PubsubClient(sleekxmpp.ClientXMPP): 27 | 28 | def __init__(self, jid, password, server, 29 | node=None, action='list', data=''): 30 | super(PubsubClient, self).__init__(jid, password) 31 | 32 | print "jid %s." % jid 33 | print "password %s." % password 34 | print "server %s." % server 35 | print "node %s." % node 36 | 37 | self.register_plugin('xep_0030') 38 | self.register_plugin('xep_0059') 39 | self.register_plugin('xep_0060') 40 | 41 | self.actions = ['nodes', 'create', 'delete', 42 | 'publish', 'get', 'retract', 43 | 'purge', 'subscribe', 'unsubscribe'] 44 | 45 | self.action = action 46 | self.node = node 47 | self.data = data 48 | self.pubsub_server = server 49 | 50 | self.add_event_handler('session_start', self.start, threaded=True) 51 | 52 | def start(self, event): 53 | self.get_roster() 54 | self.send_presence() 55 | 56 | try: 57 | getattr(self, self.action)() 58 | except: 59 | logging.error('Could not execute: %s' % self.action) 60 | self.disconnect() 61 | 62 | def create(self): 63 | try: 64 | self['xep_0060'].create_node(self.pubsub_server, self.node) 65 | except: 66 | logging.error('Could not create node: %s' % self.node) 67 | 68 | def delete(self): 69 | try: 70 | self['xep_0060'].delete_node(self.pubsub_server, self.node) 71 | print('Deleted node: %s' % self.node) 72 | except: 73 | logging.error('Could not delete node: %s' % self.node) 74 | 75 | if __name__ == '__main__': 76 | 77 | try: 78 | 79 | extension=sys.argv[1] 80 | except: 81 | 82 | print "You need to inform extension..." 83 | sys.exit(1) 84 | 85 | nodeName="extension%s" % extension 86 | 87 | configFile='/etc/asterisk/pubsub.conf' 88 | 89 | try: 90 | 91 | configuration = ConfigParser.RawConfigParser() 92 | configuration.read(configFile) 93 | 94 | xmppDomain=configuration.get('general','xmppdomain') 95 | pubsubDomain=configuration.get('general','pubsubdomain') 96 | #xmppUser="%s/Asterisk" % configuration.get('general','xmppuser') 97 | xmppUser="%s" % configuration.get('general','xmppuser') 98 | xmppSecret=configuration.get('general','xmppsecret') 99 | 100 | except: 101 | 102 | print "Config file %s not found." % configFile 103 | sys.exit(1) 104 | 105 | # Setup the Pubsub client 106 | xmpp = PubsubClient(xmppUser, xmppSecret, 107 | server=xmppUser, 108 | node=nodeName, 109 | action="create", 110 | data='') 111 | 112 | # If you are working with an OpenFire server, you may need 113 | # to adjust the SSL version used: 114 | # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 115 | 116 | # If you want to verify the SSL certificates offered by a server: 117 | # xmpp.ca_certs = "path/to/ca/cert" 118 | 119 | # Connect to the XMPP server and start processing XMPP stanzas. 120 | if xmpp.connect(): 121 | # If you do not have the dnspython library installed, you will need 122 | # to manually specify the name of the server if it does not match 123 | # the one in the JID. For example, to use Google Talk you would 124 | # need to use: 125 | # 126 | # if xmpp.connect(('talk.google.com', 5222)): 127 | # ... 128 | xmpp.process(block=True) 129 | else: 130 | print("Unable to connect.") 131 | -------------------------------------------------------------------------------- /asterisk/agi-bin/deletePubSubNode.agi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # deletePubSubNode.py 5 | # Delete PubSub Node of extension 6 | # 7 | # Marcelo H. Terres 8 | # 2016-04-10 9 | # 10 | # Based on pubsub_client.py from sleekXMPP project 11 | 12 | import sys 13 | import sleekxmpp 14 | import ConfigParser 15 | 16 | # Python versions before 3.0 do not use UTF-8 encoding 17 | # by default. To ensure that Unicode is handled properly 18 | # throughout SleekXMPP, we will set the default encoding 19 | # ourselves to UTF-8. 20 | if sys.version_info < (3, 0): 21 | from sleekxmpp.util.misc_ops import setdefaultencoding 22 | setdefaultencoding('utf8') 23 | else: 24 | raw_input = input 25 | 26 | class PubsubClient(sleekxmpp.ClientXMPP): 27 | 28 | def __init__(self, jid, password, server, 29 | node=None, action='list', data=''): 30 | super(PubsubClient, self).__init__(jid, password) 31 | 32 | print "jid %s." % jid 33 | print "password %s." % password 34 | print "server %s." % server 35 | print "node %s." % node 36 | 37 | self.register_plugin('xep_0030') 38 | self.register_plugin('xep_0059') 39 | self.register_plugin('xep_0060') 40 | 41 | self.actions = ['nodes', 'create', 'delete', 42 | 'publish', 'get', 'retract', 43 | 'purge', 'subscribe', 'unsubscribe'] 44 | 45 | self.action = action 46 | self.node = node 47 | self.data = data 48 | self.pubsub_server = server 49 | 50 | self.add_event_handler('session_start', self.start, threaded=True) 51 | 52 | def start(self, event): 53 | self.get_roster() 54 | self.send_presence() 55 | 56 | try: 57 | getattr(self, self.action)() 58 | except: 59 | logging.error('Could not execute: %s' % self.action) 60 | self.disconnect() 61 | 62 | def create(self): 63 | try: 64 | self['xep_0060'].create_node(self.pubsub_server, self.node) 65 | except: 66 | logging.error('Could not create node: %s' % self.node) 67 | 68 | def delete(self): 69 | try: 70 | self['xep_0060'].delete_node(self.pubsub_server, self.node) 71 | print('Deleted node: %s' % self.node) 72 | except: 73 | logging.error('Could not delete node: %s' % self.node) 74 | 75 | if __name__ == '__main__': 76 | 77 | try: 78 | 79 | extension=sys.argv[1] 80 | except: 81 | 82 | print "You need to inform extension..." 83 | sys.exit(1) 84 | 85 | nodeName="extension%s" % extension 86 | 87 | configFile='/etc/asterisk/pubsub.conf' 88 | 89 | try: 90 | 91 | configuration = ConfigParser.RawConfigParser() 92 | configuration.read(configFile) 93 | 94 | xmppDomain=configuration.get('general','xmppdomain') 95 | pubsubDomain=configuration.get('general','pubsubdomain') 96 | #xmppUser="%s/Asterisk" % configuration.get('general','xmppuser') 97 | xmppUser="%s" % configuration.get('general','xmppuser') 98 | xmppSecret=configuration.get('general','xmppsecret') 99 | 100 | except: 101 | 102 | print "Config file %s not found." % configFile 103 | sys.exit(1) 104 | 105 | # Setup the Pubsub client 106 | xmpp = PubsubClient(xmppUser, xmppSecret, 107 | server=xmppUser, 108 | node=nodeName, 109 | action="delete", 110 | data='') 111 | 112 | # If you are working with an OpenFire server, you may need 113 | # to adjust the SSL version used: 114 | # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 115 | 116 | # If you want to verify the SSL certificates offered by a server: 117 | # xmpp.ca_certs = "path/to/ca/cert" 118 | 119 | # Connect to the XMPP server and start processing XMPP stanzas. 120 | if xmpp.connect(): 121 | # If you do not have the dnspython library installed, you will need 122 | # to manually specify the name of the server if it does not match 123 | # the one in the JID. For example, to use Google Talk you would 124 | # need to use: 125 | # 126 | # if xmpp.connect(('talk.google.com', 5222)): 127 | # ... 128 | xmpp.process(block=True) 129 | else: 130 | print("Unable to connect.") 131 | -------------------------------------------------------------------------------- /docs/Readme: -------------------------------------------------------------------------------- 1 | Delivering Asterisk IVR data to softwares using XMPP 2 | **************************************************** 3 | 4 | Proof of concept 5 | by Marcelo Hartmann Terres 6 | Version 0.1 - 2016/04/13 7 | 8 | The project 9 | *********** 10 | 11 | The main goal of this project is to show the integration possibilities between different kind of softwares (customer service softwares, Helpdesk, CRM, etc...) with Asterisk, allowing the interoperability with IVR systems and "other softwares" used in a company. 12 | 13 | Softwares 14 | ********* 15 | 16 | This project uses the following softwares: 17 | 18 | * Asterisk - Asterisk provides the IVR and interacts with a XMPP server using an Stasis app (that uses ARI) that publishes data in a pubsub node. Also, it interacts directly with the XMPP server to announce new calls that enter in queue to all extensions logged on the queue. 19 | * XMPP server - the XMPP server, besides the IM, is used by Asterisk to announce new calls to queue members. It also works as a middleware between Asterisk and the "other software", that will use the data informed in IVR. 20 | * The "other software": the "other software" (in this PoC) is simulated by software.py script, that subscribes the pubsub node and processes all items published in the node (in this case, shows customer id and name). From a "real software" point of view, the expected behaviour demands that as soon as the user logon in the application, the software gets its linked extension (defing the pubsub node name too). After, the "real software" must connect in XMPP server (using the same Asterisk account, for example) and subscribes the node, using all items published to take actions. 21 | * PostgreSQL: PostgreSQL is used to store extensions data (number, name and xmpp account). It is also used to store customer data and to control queue behaviour (queueapp.py). 22 | 23 | 24 | From a custom point of view 25 | *************************** 26 | 27 | The customer dials and the IVR answers. 28 | 29 | IVR has 3 options: 30 | * for support, dial 1 31 | * for sales, dial 2 - call is redirected to extension 2000 32 | * please wait (no option) - call is redirected to extension 3000 33 | 34 | When customer selects option 1, it will listen a message asking to inform the customer code (4 digits). 35 | 36 | 37 | The dialed code will be validated (agi). If it is invalid, a message will be listened informing the situation. 38 | 39 | If the code is valid, Asterisk will verify the extensions logged in support queue. It will send an XMPP message to each member logged, with the customer information. 40 | 41 | After that, the call will be handled by Stasis app (queueapp.py). The app will define the destiny extension (using a leastrecent queue policy). Then the customer information will be published in the extension pubsub node (the "other software" will receive the information and should use it for its purposes). Finally, the call will be redirected to the selected extension. 42 | 43 | From the queue member point of view 44 | *********************************** 45 | 46 | The first thing that the queue member must know is that he/she needs to login and logout in the queue. To login, dial to *56466* (*LOGON*). To logout, dial to *564633*. After a few moments, the operation will be confirmed. 47 | 48 | When a new call enters the queue, the logged queue members will receive a XMPP message with the customer information. The call will be redirected to defined member. 49 | 50 | If the "other software" was integrated, now it would be the time to open the customer history form, for example. 51 | 52 | Using the PoC 53 | ************* 54 | 55 | Before use the project, follow the installation steps described in Install doc. 56 | 57 | After the installation is complete, you must access the queue directory and run queueapp.py. Leave the script open in foreground. It'll provide some interesting informations. 58 | 59 | Do the same with software.py (in software directory). 60 | 61 | Let's suppose that you will use the extensions 1000, 2000 and 3000 and that all calls to IVR will be made by extension 2000 (you can change that, but you must edit the queueapp.py script). Let's also suppose that the extensions will be linked, respectivelly with XMPP accounts user1@yourjabberdomain, user2@yourjabberdomain and user3@yourjabberdomain. 62 | 63 | The last supposition is that extension 1000 will be logged in the queue. 64 | 65 | Now, open your SIP client (extension 1000) and your XMPP client (user1@yourjabberdomain). Also, open another SIP client running extension 2000, where you will make the calls. 66 | 67 | Dial to IVR (0000) and select option 1. Enter the customer code (the initial data from database has 2 companies: Company 1 - code 1111 and Company 2 - code 2) and watch the apps messages (queueapp.py, software.py e rasterisk). You will also receive an Asterisk XMPP message with the customer data and your extension (1000) will start to ring. 68 | -------------------------------------------------------------------------------- /db/dbpgsql.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 9.5.2 6 | -- Dumped by pg_dump version 9.5.2 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET client_encoding = 'UTF8'; 11 | SET standard_conforming_strings = on; 12 | SET check_function_bodies = false; 13 | SET client_min_messages = warning; 14 | SET row_security = off; 15 | 16 | -- 17 | -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: 18 | -- 19 | 20 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 21 | 22 | 23 | -- 24 | -- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: 25 | -- 26 | 27 | COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; 28 | 29 | 30 | SET search_path = public, pg_catalog; 31 | 32 | -- 33 | -- Name: ivrdatadelivery_id_customer; Type: SEQUENCE; Schema: public; Owner: ivrdatadelivery 34 | -- 35 | 36 | CREATE SEQUENCE ivrdatadelivery_id_customer 37 | START WITH 1 38 | INCREMENT BY 1 39 | NO MINVALUE 40 | MAXVALUE 1000000 41 | CACHE 1; 42 | 43 | 44 | ALTER TABLE ivrdatadelivery_id_customer OWNER TO ivrdatadelivery; 45 | 46 | SET default_tablespace = ''; 47 | 48 | SET default_with_oids = false; 49 | 50 | -- 51 | -- Name: customer; Type: TABLE; Schema: public; Owner: ivrdatadelivery 52 | -- 53 | 54 | CREATE TABLE customer ( 55 | id bigint DEFAULT nextval('ivrdatadelivery_id_customer'::regclass) NOT NULL, 56 | name character varying(100) NOT NULL, 57 | code character(4) NOT NULL 58 | ); 59 | 60 | 61 | ALTER TABLE customer OWNER TO ivrdatadelivery; 62 | 63 | -- 64 | -- Name: ivrdatadelivery_id_queue; Type: SEQUENCE; Schema: public; Owner: ivrdatadelivery 65 | -- 66 | 67 | CREATE SEQUENCE ivrdatadelivery_id_queue 68 | START WITH 1 69 | INCREMENT BY 1 70 | NO MINVALUE 71 | MAXVALUE 1000000 72 | CACHE 1; 73 | 74 | 75 | ALTER TABLE ivrdatadelivery_id_queue OWNER TO ivrdatadelivery; 76 | 77 | -- 78 | -- Name: ivrdatadelivery_id_queuesip; Type: SEQUENCE; Schema: public; Owner: ivrdatadelivery 79 | -- 80 | 81 | CREATE SEQUENCE ivrdatadelivery_id_queuesip 82 | START WITH 1 83 | INCREMENT BY 1 84 | NO MINVALUE 85 | MAXVALUE 1000000 86 | CACHE 1; 87 | 88 | 89 | ALTER TABLE ivrdatadelivery_id_queuesip OWNER TO ivrdatadelivery; 90 | 91 | -- 92 | -- Name: ivrdatadelivery_id_sip; Type: SEQUENCE; Schema: public; Owner: ivrdatadelivery 93 | -- 94 | 95 | CREATE SEQUENCE ivrdatadelivery_id_sip 96 | START WITH 1 97 | INCREMENT BY 1 98 | NO MINVALUE 99 | MAXVALUE 1000000 100 | CACHE 1; 101 | 102 | 103 | ALTER TABLE ivrdatadelivery_id_sip OWNER TO ivrdatadelivery; 104 | 105 | -- 106 | -- Name: queue; Type: TABLE; Schema: public; Owner: ivrdatadelivery 107 | -- 108 | 109 | CREATE TABLE queue ( 110 | id bigint DEFAULT nextval('ivrdatadelivery_id_queue'::regclass) NOT NULL, 111 | name character varying(50) NOT NULL 112 | ); 113 | 114 | 115 | ALTER TABLE queue OWNER TO ivrdatadelivery; 116 | 117 | -- 118 | -- Name: queue_sip; Type: TABLE; Schema: public; Owner: ivrdatadelivery 119 | -- 120 | 121 | CREATE TABLE queue_sip ( 122 | id bigint DEFAULT nextval('ivrdatadelivery_id_queuesip'::regclass) NOT NULL, 123 | queue_id bigint NOT NULL, 124 | sip_id bigint NOT NULL, 125 | logged boolean DEFAULT false NOT NULL, 126 | lasthangup timestamp without time zone 127 | ); 128 | 129 | 130 | ALTER TABLE queue_sip OWNER TO ivrdatadelivery; 131 | 132 | -- 133 | -- Name: sip; Type: TABLE; Schema: public; Owner: ivrdatadelivery 134 | -- 135 | 136 | CREATE TABLE sip ( 137 | id bigint DEFAULT nextval('ivrdatadelivery_id_sip'::regclass) NOT NULL, 138 | extension character varying(10) NOT NULL, 139 | jid character varying(100) NOT NULL 140 | ); 141 | 142 | 143 | ALTER TABLE sip OWNER TO ivrdatadelivery; 144 | 145 | -- 146 | -- Name: customer_name_key; Type: CONSTRAINT; Schema: public; Owner: ivrdatadelivery 147 | -- 148 | 149 | ALTER TABLE ONLY customer 150 | ADD CONSTRAINT customer_name_key UNIQUE (name); 151 | 152 | 153 | -- 154 | -- Name: customer_pkey; Type: CONSTRAINT; Schema: public; Owner: ivrdatadelivery 155 | -- 156 | 157 | ALTER TABLE ONLY customer 158 | ADD CONSTRAINT customer_pkey PRIMARY KEY (id); 159 | 160 | 161 | -- 162 | -- Name: queue_pkey; Type: CONSTRAINT; Schema: public; Owner: ivrdatadelivery 163 | -- 164 | 165 | ALTER TABLE ONLY queue 166 | ADD CONSTRAINT queue_pkey PRIMARY KEY (id); 167 | 168 | 169 | -- 170 | -- Name: queue_sip_pkey; Type: CONSTRAINT; Schema: public; Owner: ivrdatadelivery 171 | -- 172 | 173 | ALTER TABLE ONLY queue_sip 174 | ADD CONSTRAINT queue_sip_pkey PRIMARY KEY (id); 175 | 176 | 177 | -- 178 | -- Name: sip_pkey; Type: CONSTRAINT; Schema: public; Owner: ivrdatadelivery 179 | -- 180 | 181 | ALTER TABLE ONLY sip 182 | ADD CONSTRAINT sip_pkey PRIMARY KEY (id); 183 | 184 | 185 | -- 186 | -- Name: ivrdatadelivery_sip_extension; Type: INDEX; Schema: public; Owner: ivrdatadelivery 187 | -- 188 | 189 | CREATE UNIQUE INDEX ivrdatadelivery_sip_extension ON sip USING btree (extension); 190 | 191 | 192 | -- 193 | -- Name: public; Type: ACL; Schema: -; Owner: postgres 194 | -- 195 | 196 | REVOKE ALL ON SCHEMA public FROM PUBLIC; 197 | REVOKE ALL ON SCHEMA public FROM postgres; 198 | GRANT ALL ON SCHEMA public TO postgres; 199 | GRANT ALL ON SCHEMA public TO PUBLIC; 200 | 201 | 202 | -- 203 | -- PostgreSQL database dump complete 204 | -- 205 | 206 | -------------------------------------------------------------------------------- /docs/Readme_ptbr: -------------------------------------------------------------------------------- 1 | Integrando URAs Asterisk com softwares de atendimento ao cliente através de XMPP 2 | ******************************************************************************** 3 | 4 | Prova de conceito 5 | by Marcelo Hartmann Terres 6 | Versão 0.1 - 2016/04/13 7 | 8 | Objetivo 9 | ******** 10 | 11 | O objetivo deste projeto é demonstrar as possibilidade de integração entre softwares diversos (SAC, Ouvidoria, HelpDesk, etc...) com URAs Asterisk. 12 | 13 | Um dos grandes problemas no atuais serviços telefônicos de atendimento ao cliente é a total falta de integração entre a URA e o sistema utilizado pelo atendente. Infelizmente ainda é muito comum você ligar para um SAC e, após ter se identificado na URA, ter que passar novamente seus dados para o atendente para que ele/ela possa lhe identificar no sistema. 14 | 15 | Em função disso, resolvi desenvolver este pequeno projeto que demonstra como é possível implementar uma integração inteligente entre a URA e o sistema de atendimento, tornando o processo mais rápido e ágil para o cliente e também para o atendente. 16 | 17 | Alguns exemplos onde este projeto pode ser utilizado: 18 | 19 | * Você ligou para o seu banco, informou seu CPF na URA, e quando o atendente começar a falar com você ele já está com seu histórico de atendimento disponível. 20 | * Você ligou para uma ouvidoria, informou o código do protocolo ao qual deseja atendimento, e quando o atendente falar com você ele já está acessando o atendimento a qual se refere o protocolo devidamente aberto, com todas as informações disponíveis. 21 | 22 | Softwares 23 | ********* 24 | 25 | O projeto depende de vários softwares distintos: 26 | 27 | * Asterisk - o Asterisk provê a URA e interage via uma app Stasis (usando ARI) com o servidor XMPP para publicação dos dados do cliente. Além disso, ele fala diretamente com o servidor XMPP para anunciar a entrada de chamadas dos clientes para os atendentes logados na fila. 28 | * Servidor XMPP - o servidor XMPP, além de ser utilizado internamente para a comunicação entre os colaboradores, é utilizado pelo Asterisk para anunciar novas chamadas de suporte entrantes na fila aos membros logados na fila. Ele também é o middleware usado para permitir a comunicação entre o Asterisk e o software de atendimento ao cliente que fará uso dos dados obtidos na URA. 29 | * O software de atendimento ao cliente: nesta PoC, o software de atendimento ao cliente é simulado por um script (software.py), que assina o nó do ramal no servidor XMPP e processa os itens publicados neste (no caso, exibe o código e o nome do cliente). Do ponto de vista de uma aplicação real, o comportamento esperado seria que assim que o usuário logasse no sistema, o software determinasse qual seu nó no serviço pubsub do servidor XMPP (no caso desta PoC, o nome do nó é formado por "Extension" + número do ramal - o número do ramal poderia ficar no cadastro do usuário no sistema). De posse deste dados, o software deveria se autenticar no servidor XMPP (usando, por exemplo, a mesma conta que é utilizada pelo servidor Asterisk) e assinar o respectivo nó, tratando as informações que fossem publicadas neste (ex: usando o código do protocolo para acessar a informação do mesmo). 30 | * PostgreSQL: o PostgreSQL é usado para armazenar os dados dos ramais (número, nome e seu vínculo com a conta XMPP). Ele também é usado para armazenar o cadastro de clientes (usado na URA) e para controlar o comportamento da fila do suporte (queueapp.py). 31 | 32 | Funcionamento do ponto de vista do cliente 33 | ****************************************** 34 | 35 | O cliente liga para o "número da empresa" e é atendido por uma URA. 36 | 37 | A URA possui 3 opções: 38 | * disque 1 para suporte 39 | * disque 2 para vendas - direciona a chamada para o ramal 2000 40 | * aguarde para ser atendido - direciona a chamada para o ramal 3000 41 | 42 | Quando o cliente selecionar a opção 1, será solicitado o código do cliente, de 4 dígitos. 43 | 44 | O código informado será consultado, via agi, no banco de dados e, caso seja inválido, o cliente será informado. 45 | 46 | Se o código for válido, o Asterisk verificará quais são os ramais que atualmente estão logados na fila support. De posse desta informações, ele enviará uma mensagem XMPP para cada um dos atendentes, informando que tem um ligação do cliente X (devidamente identificado) na fila. 47 | 48 | O Asterisk passa então a chamada para a aplicação Stasis (queueapp.py), junto com os dados do cliente. A aplicação identifica então qual o ramal de destino da chamada (usando uma política de fila leastrecent) e publica no nó do serviço PubSub deste ramal os dados do cliente enviados pelo Asterisk (neste momento, a software de atendimento ao cliente - na PoC, representado pelo script software.py) recebe tais dados e processa-os). A chamada é então direcionada para o ramal, para atendimento. 49 | 50 | Funcionamento do ponto de vista do atendente 51 | ******************************************** 52 | 53 | Do ponto de vista do atendente, a primeira coisa que o mesmo deve fazer é logar na fila support. Isso pode ser feito discando para *56466* (*LOGON*). Após alguns instantes o atendente será informado que logou na fila. 54 | 55 | De forma similar, o atendente também precisa deslogar da fila quando não for atender mais chamadas da mesma. O código para deslogar é *564633* (*LOGOFF*). 56 | 57 | Ao entrar nova chamada na fila, o atendente logado na fila irá receber uma mensagem no seu cliente XMPP, informando o nome do cliente. A chamada então será entregue para um dos atendentes logados na fila e que esteja livre. 58 | 59 | Supondo que o sistema esteja devidamente integrado, este poderia, por exemplo, abrir (ou questionar se o atendente deseja abrir) o histórico de atendimentos do cliente em questão. 60 | 61 | 62 | Utilizando o projeto 63 | ******************** 64 | 65 | Antes de mais nada siga os passos do documento de instalação, existente neste diretório. 66 | 67 | Depois de devidamente instalado, é preciso acessar o diretório queue e colocar a app queueapp.py em execução. Sugiro que você deixe a aplicação aberto, rodando em foreground, para acompanhar seus passos. 68 | 69 | Também é preciso acessar o diretório software e iniciar o script software.py, deixando-o rodando também em foreground para acompanhar seus passos. 70 | 71 | Vamos então partir da premissa que você vai usar os ramais 1000,2000 e 3000 e que as chamadas para a URA virão do ramal 2000 (você pode modificar isso como desejar, mas faça as devidas alterações no queueapp.py). Além disso, vamos supor que estes ramais estejam respectivamente vinculados as contas XMPP user1@seudominiojabber, user1@seudominiojabber e user3@seudominiojabber. 72 | 73 | Vamos supor também que o ramal 1000 irá ser o atendente da fila (lembre-se que você pode ter vários atendentes). 74 | 75 | Logue-se então no seu cliente SIP com o ramal 1000 e no seu cliente XMPP com a conta respectiva vinculada a este ramal. 76 | 77 | Logue-se também em outro cliente SIP com o ramal 2000, para efetuar as chamadas para a URA. 78 | 79 | Disque para a URA (0000) e selecione a opção 1. Informe o código do cliente (existem 2 clientes na base modelo, Company 1 - código 1111 e Company 2 - código 2) e acompanhe o que acontece nas aplicações (queueapp.py, software.py e rasterisk). Além dos logs nestas aplicações, você deverá receber uma mensagem XMPP do Asterisk informando da chamada do cliente e também a ligação no ramal 1000. 80 | -------------------------------------------------------------------------------- /tests/pubsub_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import logging 6 | import getpass 7 | from optparse import OptionParser 8 | 9 | import sleekxmpp 10 | from sleekxmpp.xmlstream import ET, tostring 11 | 12 | 13 | # Python versions before 3.0 do not use UTF-8 encoding 14 | # by default. To ensure that Unicode is handled properly 15 | # throughout SleekXMPP, we will set the default encoding 16 | # ourselves to UTF-8. 17 | if sys.version_info < (3, 0): 18 | from sleekxmpp.util.misc_ops import setdefaultencoding 19 | setdefaultencoding('utf8') 20 | else: 21 | raw_input = input 22 | 23 | 24 | class PubsubClient(sleekxmpp.ClientXMPP): 25 | 26 | def __init__(self, jid, password, server, 27 | node=None, action='list', data=''): 28 | super(PubsubClient, self).__init__(jid, password) 29 | 30 | self.register_plugin('xep_0030') 31 | self.register_plugin('xep_0059') 32 | self.register_plugin('xep_0060') 33 | 34 | self.actions = ['nodes', 'create', 'delete', 35 | 'publish', 'get', 'retract', 36 | 'purge', 'subscribe', 'unsubscribe'] 37 | 38 | self.action = action 39 | self.node = node 40 | self.data = data 41 | self.pubsub_server = server 42 | 43 | self.add_event_handler('session_start', self.start, threaded=True) 44 | 45 | def start(self, event): 46 | self.get_roster() 47 | self.send_presence() 48 | 49 | try: 50 | getattr(self, self.action)() 51 | except: 52 | logging.error('Could not execute: %s' % self.action) 53 | self.disconnect() 54 | 55 | def nodes(self): 56 | try: 57 | result = self['xep_0060'].get_nodes(self.pubsub_server, self.node) 58 | for item in result['disco_items']['items']: 59 | print(' - %s' % str(item)) 60 | except: 61 | logging.error('Could not retrieve node list.') 62 | 63 | def create(self): 64 | try: 65 | self['xep_0060'].create_node(self.pubsub_server, self.node) 66 | except: 67 | logging.error('Could not create node: %s' % self.node) 68 | 69 | def delete(self): 70 | try: 71 | self['xep_0060'].delete_node(self.pubsub_server, self.node) 72 | print('Deleted node: %s' % self.node) 73 | except: 74 | logging.error('Could not delete node: %s' % self.node) 75 | 76 | def publish(self): 77 | payload = ET.fromstring("%s" % self.data) 78 | try: 79 | result = self['xep_0060'].publish(self.pubsub_server, self.node, payload=payload) 80 | id = result['pubsub']['publish']['item']['id'] 81 | print('Published at item id: %s' % id) 82 | except: 83 | logging.error('Could not publish to: %s' % self.node) 84 | 85 | def get(self): 86 | try: 87 | result = self['xep_0060'].get_item(self.pubsub_server, self.node, self.data) 88 | for item in result['pubsub']['items']['substanzas']: 89 | print('Retrieved item %s: %s' % (item['id'], tostring(item['payload']))) 90 | except: 91 | logging.error('Could not retrieve item %s from node %s' % (self.data, self.node)) 92 | 93 | def retract(self): 94 | try: 95 | result = self['xep_0060'].retract(self.pubsub_server, self.node, self.data) 96 | print('Retracted item %s from node %s' % (self.data, self.node)) 97 | except: 98 | logging.error('Could not retract item %s from node %s' % (self.data, self.node)) 99 | 100 | def purge(self): 101 | try: 102 | result = self['xep_0060'].purge(self.pubsub_server, self.node) 103 | print('Purged all items from node %s' % self.node) 104 | except: 105 | logging.error('Could not purge items from node %s' % self.node) 106 | 107 | def subscribe(self): 108 | try: 109 | result = self['xep_0060'].subscribe(self.pubsub_server, self.node) 110 | print('Subscribed %s to node %s' % (self.boundjid.bare, self.node)) 111 | except: 112 | logging.error('Could not subscribe %s to node %s' % (self.boundjid.bare, self.node)) 113 | 114 | def unsubscribe(self): 115 | try: 116 | result = self['xep_0060'].unsubscribe(self.pubsub_server, self.node) 117 | print('Unsubscribed %s from node %s' % (self.boundjid.bare, self.node)) 118 | except: 119 | logging.error('Could not unsubscribe %s from node %s' % (self.boundjid.bare, self.node)) 120 | 121 | 122 | 123 | 124 | if __name__ == '__main__': 125 | # Setup the command line arguments. 126 | optp = OptionParser() 127 | optp.version = '%%prog 0.1' 128 | optp.usage = "Usage: %%prog [options] " + \ 129 | 'nodes|create|delete|purge|subscribe|unsubscribe|publish|retract|get' + \ 130 | ' [ ]' 131 | 132 | optp.add_option('-q','--quiet', help='set logging to ERROR', 133 | action='store_const', 134 | dest='loglevel', 135 | const=logging.ERROR, 136 | default=logging.ERROR) 137 | optp.add_option('-d','--debug', help='set logging to DEBUG', 138 | action='store_const', 139 | dest='loglevel', 140 | const=logging.DEBUG, 141 | default=logging.ERROR) 142 | optp.add_option('-v','--verbose', help='set logging to COMM', 143 | action='store_const', 144 | dest='loglevel', 145 | const=5, 146 | default=logging.ERROR) 147 | 148 | # JID and password options. 149 | optp.add_option("-j", "--jid", dest="jid", 150 | help="JID to use") 151 | optp.add_option("-p", "--password", dest="password", 152 | help="password to use") 153 | opts,args = optp.parse_args() 154 | 155 | # Setup logging. 156 | logging.basicConfig(level=opts.loglevel, 157 | format='%(levelname)-8s %(message)s') 158 | 159 | if len(args) < 2: 160 | optp.print_help() 161 | exit() 162 | 163 | if opts.jid is None: 164 | opts.jid = raw_input("Username: ") 165 | if opts.password is None: 166 | opts.password = getpass.getpass("Password: ") 167 | 168 | if len(args) == 2: 169 | args = (args[0], args[1], '', '', '') 170 | elif len(args) == 3: 171 | args = (args[0], args[1], args[2], '', '') 172 | elif len(args) == 4: 173 | args = (args[0], args[1], args[2], args[3], '') 174 | 175 | 176 | # Setup the Pubsub client 177 | xmpp = PubsubClient(opts.jid, opts.password, 178 | server=args[0], 179 | node=args[2], 180 | action=args[1], 181 | data=args[3]) 182 | 183 | # If you are working with an OpenFire server, you may need 184 | # to adjust the SSL version used: 185 | # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 186 | 187 | # If you want to verify the SSL certificates offered by a server: 188 | # xmpp.ca_certs = "path/to/ca/cert" 189 | 190 | # Connect to the XMPP server and start processing XMPP stanzas. 191 | if xmpp.connect(): 192 | # If you do not have the dnspython library installed, you will need 193 | # to manually specify the name of the server if it does not match 194 | # the one in the JID. For example, to use Google Talk you would 195 | # need to use: 196 | # 197 | # if xmpp.connect(('talk.google.com', 5222)): 198 | # ... 199 | xmpp.process(block=True) 200 | else: 201 | print("Unable to connect.") 202 | -------------------------------------------------------------------------------- /queue/queueapp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # queueapp.py 5 | # QueueApp for Asterisk integrated with XMPP Server for PubSub use 6 | # 7 | # Marcelo H. Terres 8 | # 2016-04-11 9 | # 10 | 11 | import os 12 | import ari 13 | import sys 14 | import time 15 | import uuid 16 | import logging 17 | import requests 18 | import sleekxmpp 19 | import threading 20 | import ConfigParser 21 | 22 | import psycopg2 23 | import psycopg2.extras 24 | 25 | from sleekxmpp.xmlstream import ET, tostring 26 | 27 | #logging.basicConfig() 28 | 29 | # Python versions before 3.0 do not use UTF-8 encoding 30 | # by default. To ensure that Unicode is handled properly 31 | # throughout SleekXMPP, we will set the default encoding 32 | # ourselves to UTF-8. 33 | if sys.version_info < (3, 0): 34 | from sleekxmpp.util.misc_ops import setdefaultencoding 35 | setdefaultencoding('utf8') 36 | else: 37 | raw_input = input 38 | 39 | configFile='%s/queueapp.conf' % os.path.dirname(sys.argv[0]) 40 | 41 | try: 42 | 43 | configuration = ConfigParser.RawConfigParser() 44 | configuration.read(configFile) 45 | 46 | # queue config 47 | queue_id=configuration.get('queue','id') 48 | queue_name=configuration.get('queue','name') 49 | queue_strategy=configuration.get('queue','strategy') 50 | queue_timeout="%s/Asterisk" % configuration.get('queue','timeout') 51 | queue_ringtimeperexten=configuration.get('queue','ringtimeperexten') 52 | queue_extension=configuration.get('queue','extension') 53 | 54 | # xmpp config 55 | xmppUser=configuration.get('xmpp','xmppUser') 56 | xmppSecret=configuration.get('xmpp','xmppSecret') 57 | pubsubDomain=configuration.get('xmpp','pubsubDomain') 58 | 59 | # asterisk ari config 60 | ari_user=configuration.get('ari','user') 61 | ari_secret=configuration.get('ari','secret') 62 | 63 | # db config 64 | db_name=configuration.get('db','name') 65 | db_host=configuration.get('db','host') 66 | db_user=configuration.get('db','user') 67 | db_secret=configuration.get('db','secret') 68 | 69 | except: 70 | 71 | print "Config file %s not found." % configFile 72 | sys.exit(1) 73 | 74 | # DEBUG 75 | log = open('/tmp/queueapp.log', 'a') 76 | sys.stderr = log 77 | 78 | class XMPP(sleekxmpp.ClientXMPP): 79 | 80 | def __init__(self, jid, password, pubsubDomain): 81 | sleekxmpp.ClientXMPP.__init__(self, jid, password) 82 | 83 | self.pubsub=pubsubDomain 84 | self.register_plugin('xep_0060') 85 | 86 | # The session_start event will be triggered when 87 | # the bot establishes its connection with the server 88 | # and the XML streams are ready for use. We want to 89 | # listen for this event so that we we can initialize 90 | # our roster. 91 | self.add_event_handler("session_start", self.start) 92 | 93 | def start(self, event): 94 | """ 95 | Process the session_start event. 96 | """ 97 | self.send_presence() 98 | 99 | def publish(self,data,node): 100 | 101 | # publish data in pubsub node 102 | 103 | #print "publish item %s in node %s on %s" % (data,node,self.pubsub) 104 | 105 | payload = ET.fromstring("%s" % data) 106 | try: 107 | result = self['xep_0060'].publish(self.pubsub, node, payload=payload) 108 | id = result['pubsub']['publish']['item']['id'] 109 | print('Published at item id: %s' % id) 110 | except: 111 | print('Could not publish to: %s on %s' % (node,self.pubsub)) 112 | 113 | class DBPgsql: 114 | 115 | def __init__(self,db_name,db_host,db_user,db_secret): 116 | 117 | self.dsn = 'dbname=%s host=%s user=%s password=%s' % (db_name,db_host,db_user,db_secret) 118 | 119 | self.conn = psycopg2.connect(self.dsn) 120 | 121 | def updateData(self,queue_id,extension): 122 | 123 | # update last hangup time of queue member 124 | 125 | curs = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 126 | 127 | #print "Updating queue data in database - extension %s" % extension 128 | 129 | sql = "SELECT id FROM sip WHERE extension = '%s';" % extension 130 | curs.execute(sql) 131 | 132 | if not curs.rowcount: 133 | print("Can't find extension %s" % extension) 134 | else: 135 | 136 | sip_id = curs.fetchone()['id'] 137 | 138 | sql = "SELECT * FROM queue_sip WHERE queue_id=%s and sip_id=%s;" % (queue_id,sip_id) 139 | curs.execute(sql) 140 | 141 | if curs.rowcount: 142 | 143 | sql="UPDATE queue_sip SET lasthangup='%s' WHERE queue_id=%s and sip_id=%s;" % (time.strftime("%Y-%m-%d %H:%M:%S"),queue_id,sip_id) 144 | curs.execute(sql) 145 | self.conn.commit() 146 | 147 | def selectExten(self,queue_id): 148 | 149 | # select extension to redirect call 150 | 151 | curs = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 152 | 153 | #print "Selecting extension to redirect call" 154 | 155 | sql = "SELECT sip.extension as exten FROM sip,queue_sip WHERE queue_sip.queue_id=%s AND queue_sip.logged=True AND sip.id=queue_sip.sip_id ORDER BY lasthangup LIMIT 1;" % queue_id 156 | curs.execute(sql) 157 | 158 | if not curs.rowcount: 159 | print("Can't find valid extension to queue %s" % queue_id) 160 | else: 161 | 162 | exten=curs.fetchone()['exten'] 163 | 164 | self.conn.commit() 165 | return exten 166 | 167 | def safe_hangup(channel,extension): 168 | """Safely hang up the specified channel""" 169 | db.updateData(queue_id,extension.split("/")[1]) 170 | print "Call ended at %s\n" % time.strftime("%c") 171 | try: 172 | channel.hangup() 173 | print "Hung up {}".format(channel.json.get('name')) 174 | except requests.HTTPError as e: 175 | if e.response.status_code != requests.codes.not_found: 176 | raise e 177 | 178 | def safe_bridge_destroy(bridge): 179 | """Safely destroy the specified bridge""" 180 | try: 181 | bridge.destroy() 182 | except requests.HTTPError as e: 183 | if e.response.status_code != requests.codes.not_found: 184 | raise e 185 | 186 | def stasis_start_cb(channel_obj, ev): 187 | """Handler for StasisStart""" 188 | 189 | def playback_finished(playback, ev): 190 | """Callback when the playback have finished""" 191 | 192 | target_uri = playback.json.get('target_uri') 193 | channel_id = target_uri.replace('channel:', '') 194 | channel = client.channels.get(channelId=channel_id) 195 | 196 | channel.ring() 197 | 198 | exten=db.selectExten(queue_id) 199 | extension='PJSIP/%s' % exten 200 | nodeName="extension%s" % exten 201 | 202 | xmpp.publish(args[0],nodeName) 203 | 204 | try: 205 | print "Dialing %s - Origin %s" % (extension,callerid) 206 | outgoing = client.channels.originate(endpoint=extension, 207 | app='queueapp', 208 | appArgs='dialed', 209 | callerId=callerid) 210 | 211 | except requests.HTTPError: 212 | print "Whoops, pretty sure %s wasn't valid" % extension 213 | channel.hangup() 214 | return 215 | 216 | channel.on_event('StasisEnd', lambda *args: safe_hangup(outgoing,extension)) 217 | outgoing.on_event('StasisEnd', lambda *args: safe_hangup(channel,extension)) 218 | 219 | def outgoing_start_cb(channel_obj, ev): 220 | """StasisStart handler for our dialed channel""" 221 | 222 | print "{} answered; bridging with {}".format(outgoing.json.get('name'), 223 | channel.json.get('name')) 224 | 225 | channel.answer() 226 | 227 | bridge = client.bridges.create(type='mixing') 228 | bridge.addChannel(channel=[channel.id, outgoing.id]) 229 | 230 | # Clean up the bridge when done 231 | channel.on_event('StasisEnd', lambda *args: 232 | safe_bridge_destroy(bridge)) 233 | outgoing.on_event('StasisEnd', lambda *args: 234 | safe_bridge_destroy(bridge)) 235 | 236 | outgoing.on_event('StasisStart', outgoing_start_cb) 237 | 238 | channel = channel_obj.get('channel') 239 | callerid = channel.json.get('caller')['number'] 240 | 241 | # provide callerid (in case of receiving iax calls) 242 | if len(callerid)==0: 243 | callerid="Asterisk" 244 | 245 | channel_name = channel.json.get('name') 246 | args = ev.get('args') 247 | 248 | # get data received from Asterisk Stasis App (dialplan) 249 | pubsubData=args[0] 250 | 251 | print "%s - {} entered our application".format(channel_name) % time.strftime("%c") 252 | print "Pubsub data: %s" % pubsubData 253 | 254 | channel.answer() 255 | 256 | channel.on_event('StasisEnd', lambda *args: safe_hangup(outgoing,"")) 257 | 258 | chanType=channel.json.get('name').split('-')[0] # get CHANTYPE/EXTEN 259 | #chanType=channel.json.get('name').split('/')[0] # get CHANTYPE 260 | 261 | # Normally you will avoid process of a call when channel is PJSIP/SIP 262 | # I did this because I want to process only external calls 263 | # Also, I want to avoid that the originate call be processed too (loop) 264 | 265 | #if chanType != "PJSIP": # avoid process when channel type is PJSIP 266 | 267 | # I'm using PJSIP/2000 for my tests, but you should consider using ChanType != "PJSIP" 268 | if chanType == "PJSIP/2000": # process when channel is PJSIP/2000 269 | 270 | playback_id = str(uuid.uuid4()) 271 | playback = channel.playWithId(playbackId=playback_id,media='sound:poc/support_team') 272 | playback.on_event('PlaybackFinished', playback_finished) 273 | 274 | print "queueapp started at %s" % time.strftime("%c") 275 | 276 | print "Asterisk ARI - connecting" 277 | client = ari.connect('http://localhost:8088', ari_user, ari_secret) 278 | client.on_channel_event('StasisStart', stasis_start_cb) 279 | 280 | print "XMPP Server - connecting" 281 | xmpp=XMPP(xmppUser,xmppSecret,pubsubDomain) 282 | xmpp.connect() 283 | xmpp.process(block=False) 284 | 285 | print "DB PostgreSQL - connecting" 286 | db=DBPgsql(db_name,db_host,db_user,db_secret) 287 | 288 | print "Waiting for calls..." 289 | 290 | client.run(apps='queueapp') 291 | 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------