23 July 2013

Grails - Creating an Auto Dialer

I had a need for an internal tool where I work.

The premise is that our company buys and stores various numbers that they lease out later.  However, if those numbers do not have activity in a certain amount of time, they can become deactivated.

In the past, the company had individuals who would (every few months) get a list of current numbers in the risk zone for deactivation (hundreds of numbers) and dial each one to make sure it still went to our menu. 

So the Provisioning Team reached out to me to ask me if this could be automated....

They're requirements were that:
    a) they have lists of phone numbers (DNIS) that need to be called. this list could be a CSV
    b) they would like to upload it to a tool which would dial each number validating if it was active or not
    c) they would be notified of the results somehow.

I put together this tool and here's how I did it...

Basically I built an auto dialer with Grails and SIPCLI.  I could have used SIPP, but since I foresaw a future need to do text to speech, I opted for sipcli.  The downside to sipcli is that it is a windows tool... so you're tied to the Windows Env in this case.  But you could easily repurpose the methodology here to work with Linux using SIPP instead.

Using Grails, I created an application with:
grails create-app dialer
then I cd'd into the dialer directory and created the main controller:
grails create-controller csvImport
Once done I opened up my IDE and imported the Grails project (I use Grails Tools Suite.)

View

In the View I did this to capture the user's CSV file:
        <div id="status" role="complementary">
            <h1>Upload your CSV file of phone numbers</h1>
            <g:form controller="CsvImport" method="post" action="save"
                enctype ="multipart/form-data">

                   E-mail Results to a @mydomain.com address:<br>
                <g:textField type="field" name="emailAdd" value="brian@mydomain.com" style='width: 500px;' required=""/><p/>
                <input type="file" name="myfile" required/>
                <g:actionSubmit value="Start Process" action="save"/>
            </g:form>
        </div>


The controller looks like this:
   def save() {
        def emailTo = params.emailAdd
           if (emailTo =~ /@mydomain.com/) {
        def csv = request.getFile('myfile').inputStream.text
       
        def data = parseCsv(csv)
        runAsync{
            for(line in data) {
            println "Trying... $line.Phone"
            def dialNum = "sipcli/sipcli.exe $line.Phone -d **.**.*.*** [masked IP of our proxy] -o 4 -t \"This is a test. this is a test. this is a test. this is a test\"-l 3".execute()
            def outFile = new File("grails-app/test.txt")
            if (dialNum.text =~ /success/){               
                outFile << ("PASS on number $line.Phone\r\n")

            } else {
                System.getProperty("line.separator")
                outFile.append("FAIL on number $line.Phone\r\n")

            }
            }
 
            sendMail {
                multipart true
                to "${emailTo}"
                from "SOMEONE@ADDRESS.COM"
                subject "Provisioning Report"
                body 'Please find the attached Provisioning Report...'
                attachBytes 'grails-app/test.txt','text/csv', new File('grails-app/test.txt').readBytes()
            }

} else {
   render "Error: Email does not conform to @mydomain.com" }
            }
           
         }


To explain how it works...
The view is pretty easy to understand, it just takes a file and passes it to the controller.

Controller

The controller isn't doing any special validation, since this is a internal tool.  It takes the CSV and expects to have a column header called "Phone."  Phone will have a list of DNIS (or numbers) that need to be called.


CSV Parsing

To parse the CSV I import:
@Grab('com.xlson.groovycsv:groovycsv:1.0')
import static com.xlson.groovycsv.CsvParser.parseCsv

Then use this call def csv = request.getFile('myfile').inputStream.text to grab the input and finally pass it to the parser:
def data = parseCsv(csv)

Running Asynchronously

I don't want the  user to wait 30min for 500 phone numbers to be dialed... so instead, I send them immediately to a  page... this is handled with runAsync... this is a plugin called executor.  The Grails Executor plugin will run a closure asynchronously so you can do other stuff while the longer method runs.

Inside the runAsync closure is this for loop and if statement:
            for(line in data) {
            println "Trying... $line.Phone"
            def dialNum = "sipcli/sipcli.exe $line.Phone -d **.**.*.*** [masked IP of our proxy] -o 4 -t \"This is a test. this is a test. this is a test. this is a test\"-l 3".execute()
            def outFile = new File("grails-app/test.txt")
            if (dialNum.text =~ /success/){               
                outFile << ("PASS on number $line.Phone\r\n")
               
               
            } else {
                System.getProperty("line.separator")
                outFile.append("FAIL on number $line.Phone\r\n")

            }


Basically it's doing this:

For each line in the csv file, it prints out the phone number it's trying, and then I've defined an action to run the sipcli sip client.... to call that same number.  SipCli is a awesome command line sip tool for windows that can be used to find sip problems and issues.  It's very light and easy to use.  In this case I have it set to a 4 second timeout and i'm telling it to read the text, "This is a test..." when it makes the phone connection.

Assertions

The assertion of whether or not the phone call is valid is via the regex i'm doing in the if statement... If dialNum.text has success then we output to a file "Pass on number [DNIS]"
However, if the number fails to connect, we append to the same file "FAIL on number [DNIS]"

E-Mail

Finally, at the end, outside the runAsync closure, I do a call to email the results (that flat file) to a recipient, using the Grails mail plugin.  I'm passing the To value from the form... and as you may have seen, I'm forcing the tool to only work if an internal email is passed through.  The domain "mydomain.com" is a filler for the real domain I check. Since the tool uses an internal SMTP server, we can't send to outside emails... so I dont want the user to violate the SMTP capabilities.  Their email address entered (if from the right domain) is added to the To line below:
             sendMail {
                multipart true
                to "${emailTo}"
                from "SOMEONE@ADDRESS.COM"
                subject "Provisioning Report"
                body 'Please find the attached Provisioning Report...'
                attachBytes 'grails-app/test.txt','text/csv', new File('grails-app/test.txt').readBytes()
            }


That's it.