#Switchy NSG IVR Outbound # #Author: Nenad Corbic # #This application is designed to demonstrate switchy capabilities #and ease of use. The pplication will connect to actively running #NSG application on the local system. It will then originate #a single call and play introductory ivr message. From then #on application will wait for user input via DTMF. # #All user logic should be defined in CallLogic Class. # #Variables # self. are global in nature. # call.vars.['var_name'] should be used for per call info and state # #Switchy Documentation # https://github.com/sangoma/switchy/blob/master/switchy/models.py # class: Session Event Call Job # # https://github.com/sangoma/switchy/blob/master/switchy/observe.py # class: EventListener Client # #Sample Switchy Applications # https://switchy.readthedocs.org/en/latest/apps.html # #License: # BSD License # http://opensource.org/licenses/bsd-license.php # # Copyright (c) 2015, Sangoma Technologies Inc # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # 1. Developer makes use of Sangoma NetBorder Gateway or Sangoma Session Border Controller # 2. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # 3. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import sched, time import switchy import threading #from threading import Timer from switchy.marks import event_callback from switchy.apps.players import TonePlay from switchy import get_originator #from fast_sched import FastScheduler #from switchy.apps.measure import CallMetrics #Enable logging to stderr log = switchy.utils.log_to_stderr() #Debug levels: INFO in production, DEBUG in devel log.setLevel("INFO") #Specify NSG IP information #In this example the Switchy sample app is running on #NSG appliance hence the use of local address host = "10.10.26.33" port = 8821 #IVR Call Logic #This is the user IVR logic #All custom development should be done here. #Variables # self. are global in nature. # call.vars.['var_name'] should be used for per call info and state # class IVRCallLogic(object): """Play a 'milli-watt' tone on the outbound leg and echo it back on the inbound """ def prepost(self, client, listener): #Get the install directory of NSG and append recording to it self.recdir = client.cmd('global_getvar base_dir') self.recdir += "/recording" log.info("Setting recording dir to '{}" . format(self.recdir)) #Get the install directory of NSG and append sounds to it self.sound_dir = client.cmd('global_getvar base_dir') self.sound_dir += "/sounds" log.info("Setting sounds dir to '{}" . format(self.sound_dir)) self.stereo=False try: client.cmd('load mod_sndfile') except: pass self.timer = switchy.utils.Timer() #self.dtmf_timeout = switchy.utils.Timer() self.dtmf_timeout_period = 3.0 # process originate events the same as create events since on some versions # of FS originate events will arrive first (which is super stupid) listener._handlers.pop("CHANNEL_ORIGINATE") listener.add_handler('CHANNEL_ORIGINATE', listener._handle_create) @event_callback('CHANNEL_PARK') def on_park(self, sess): if sess.is_inbound(): sess.answer() @event_callback('CHANNEL_ORIGINATE') def on_originate(self, sess): log.info("'{}': got ORIGINATE ". format(sess.uuid)) @event_callback("CHANNEL_ANSWER") def on_answer(self, sess): call = sess.call # inbound leg simply echos back the tone if sess.is_inbound(): sess.echo() # play infinite tones on calling leg if sess.is_outbound(): #Start recording a call call.vars['record']=True self.rec_filename = '{}/callee_{}.wav'.format(self.recdir, sess.uuid) sess.start_record(self.rec_filename, stereo=self.stereo) #Play initial greeting call.vars['play_welcome'] = True play_filename = '{}/en/us/callie/ivr/8000/ivr-welcome.wav'.format(self.sound_dir) sess.playback(play_filename) def dtmf_timeout_action(self,sess): call = sess.call log.info("'{}': DTMF timeout". format(sess.uuid)) if call.vars.get('playing') == True: sess.breakmedia() call.vars['incoming_dtmf'] = None #self.dtmf_timeout.reset() play_filename = '{}/en/us/callie/ivr/8000/ivr-hello.wav'.format(self.sound_dir) call.vars['playing'] = True sess.playback(play_filename) call.vars['dtmf_timeout_job'] = threading.Timer(3, self.dtmf_timeout_action, [self,sess]) @event_callback('DTMF') def on_digit(self, sess): call = sess.call digit = int(sess['DTMF-Digit']) if call.vars.get('playing') == True: sess.breakmedia() if call.vars.get('dtmf_timeout_job'): log.debug("'{}': Cancel dtmf timeout job" . format(sess.uuid)) call.vars.get('dtmf_timeout_job').cancel() call.vars['dtmf_timeout_job'] = None call.vars['dtmf_timeout_job'] = threading.Timer(3, self.dtmf_timeout_action, [sess]) call.vars.get('dtmf_timeout_job').start() #elapsed=self.dtmf_timeout.elapsed() #if elapsed >= self.dtmf_timeout_period: # log.info("'{}': Resetting DTMF queue: timeout" . format(sess.uuid)) # call.vars['incoming_dtmf'] = None # self.dtmf_timeout.reset() log.info("'{}': DTMF dtmf digit '{}'". format(sess.uuid, digit)) if call.vars.get('incoming_dtmf') == None: call.vars['incoming_dtmf'] = str(digit) else: call.vars['incoming_dtmf'] += str(digit) if call.vars.get('incoming_dtmf') == '911': log.info("'{}': Playing 911 file STARTED" . format(sess.uuid)) call.vars['incoming_dtmf'] = None play_filename = '{}/en/us/callie/ivr/8000/ivr-contact_system_administrator.wav'.format(self.sound_dir) call.vars['playing'] = True sess.playback(play_filename) if call.vars.get('incoming_dtmf') == '811': log.info("'{}': Playing 811 file STARTED" . format(sess.uuid)) call.vars['incoming_dtmf'] = None play_filename = '{}/en/us/callie/ivr/8000/ivr-hello.wav'.format(self.sound_dir) call.vars['playing'] = True sess.playback(play_filename) if call.vars.get('incoming_dtmf') == '111': log.info("'{}': User chose to hangup" . format(sess.uuid)) call.vars['incoming_dtmf'] = None sess.hangup() if call.vars.get('incoming_dtmf') != None and len(call.vars['incoming_dtmf']) >= 3: log.debug("'{}': Resetting DTMF queue" . format(sess.uuid)) call.vars['incoming_dtmf'] = None #self.dtmf_timeout.reset() @event_callback("PLAYBACK_START") def on_playback_start(self, sess): call = sess.call log.info("'{}': got PLAYBACK_START ". format(sess.uuid)) if call.vars.get('play_welcome') == True: log.info("'{}': Playing Welcome STARTED" . format(sess.uuid)) @event_callback("PLAYBACK_STOP") def on_playback_stop(self, sess): call = sess.call log.info("'{}': got PLAYBACK_STOP ". format(sess.uuid)) call.vars['playing'] = False if call.vars.get('play_welcome') == True: call.vars['play_welcome']=False log.info("'{}': Playing Welcome STOPPED, Lets Wait for Digits" . format(sess.uuid)) @event_callback("RECORD_START") def on_playback_start(self, sess): call = sess.call log.info("'{}': got RECORD_START ". format(sess.uuid)) @event_callback("RECORD_STOP") def on_playback_stop(self, sess): call = sess.call log.info("'{}': got RECORD_STOP ". format(sess.uuid)) @event_callback('CHANNEL_HANGUP') def on_hangup(self, sess, job): call = sess.call log.info("'{}': got HANGUP ". format(sess.uuid)) if call.vars.get('play_welcome') == True: call.vars['play_welcome']=False log.info("'{}': Got HANGUP while playing" . format(sess.uuid)) #job.events.pprint() #sess.events.pprint() # Make an outbound call via NSG # SIP Call # dest_url='did@domain.com' #Remote SIP URI # dest_profile='internal' #NSG defined SIP profile name # dest_endpoint='sofia' #For SIP calls one MUST set sofia # # FreeTDM Call # dest_url='[A,a]/did' #A=ascedning hunt, a=descending hunt, DID number # dest_profile='g1' #profile is used as trunk group definition: g1 == group 1 # endpoint='freetdm" #For TDM calls on MUST set freetdm # def create_url(): log.info("Create URL") #Make a FreeTDM SS7/PRI Call #Adding F at the end of the DID disables remote SS7 overlap dialing which can add 5 sec to the incoming call setup time #Note: Developer is suppose to supply their own DID. From a list or a DB return {'dest_url': 'a/4113F', 'dest_profile': 'g1', 'dest_endpoint': 'freetdm'} #Make a SIP Call #return {'dest_url': '4113@10.10.12.5:6060', 'dest_profile': 'internal', 'dest_endpoint': 'sofia'} #Create an originator #Originator is a dialer originator = get_originator([(host,port)], apps=(IVRCallLogic,), auto_duration=False, rep_fields_func=create_url) #Setup the outbound call setup information #Destination URL originator.pool.clients[0].set_orig_cmd( dest_url='{dest_url}', profile='{dest_profile}', endpoint='{dest_endpoint}', app_name='park', ) #Campaing information max_calls_per_campaign=2 max_campaigns=1 campaign_cnt=0 #Setup calls per sec originator.rate = 1 #Setup maximum number of calls to make originator.limit = max_calls_per_campaign #Maximum number of calls to dial out originator.max_offered = max_calls_per_campaign #Start the initial campaign #Originator will start making calls to NSG originator.start() while (True): log.debug("Originator Stopped='{}' State='{}' Call Count='{}'\n". format(originator.stopped(), originator.state, originator.count_calls())) if originator.state == "STOPPED" and originator.count_calls() == 0: #increment the campaign count campaign_cnt+=1 if campaign_cnt >= max_campaigns: break #Restart new campaign log.info("Restarting new campaign\n") originator.max_offered += max_calls_per_campaign originator.start() time.sleep(1) log.info("All calls stopped, now what?\n") originator.shutdown() log.info("Stopping application\n")