Dial In IVR w/ Voice CAPTCHA & In Call Assistant

This is a simple dial-in IVR that implements a voice CAPTCHA and a in-call assistant example.

Introduction

This is a simple dial in IVR that will use voice CAPTCHA in order to determine if a caller is human or a spammer through the use of a simple math question. If they are a spammer, we will play a short message to acknowledge it and end the call. If they are human, we will forward them to the To number set in the .env file. If you would like to use this voice CAPTCHA to enhance your normal call flow, just set the To number to your normal number for handling incoming calls! We will also make sure to add the caller's number to either a list of known humans or a list of known spammers, depending on what the CAPTCHA determines.

Configuring the code

Each time a call comes in, we will check to see if the number calling is associated in our records as a known human or known spammer first. In order to access these records, we first need to create an array to store the data!

def ready
    @humans = []
    @spammers = []
  end

At the end of the CAPTCHA, each caller is confirmed to be either human or spammer. Once determined, we will add them to the correct array. You can see this implemented below:

  def known_caller?(call)
    @humans.include?(call.from)
  end


  def known_spammer?(call)
    @spammers.include?(call.from)
  end

Our next function will be responsible for handling the Voice CAPTCHA that makes the final determination if our caller is a spammer or a regular human! This CAPTCHA will use all numbers to make sure there are not any errors due to pronunciation or misunderstanding of words. We will initialize our iterator tries to 0 and set max_tries to 2. We will start with is_spammer being set to false and set it to true in the event our caller is not human. We will also define numbers 1 through 20 as a hint, so our speech recognition knows what to expect as an answer.

We begin the CAPTCHA and tell the user it is time to prove they are a human. As long as we are not at the max amount of tries (2), we will generate two random numbers between 1 and 10 to be used in our CAPTCHA. Next, we will ask the user what the sum of the two numbers is and gather their input. If the user answers correctly, is_spammer will stay equivalent to false. If the user answers incorrectly, we will set is_spammer to true, increment tries, and try again as long as the number of attempts is less than max_tries.

At the end of the max attempts, the user is either confirmed spammer or human. We will add the call's From number to the human array or spammer array and then call the functions for handling spammers or forwarding the human to the correct call flow.

  def handle_captcha(call)
    tries = 0
    max_tries = 2
    is_spammer = false
    hints = (1..20).to_a

    call.play_tts text: "Hello! Prove to me you are a human."

    while tries < max_tries do
      first_num = rand(1..10)
      second_num = rand(1..10)

      input = call.prompt_tts(type: 'both',
        digits_max: 2,
        digit_timeout: 1.0,
        digits_terminators: '#',
        end_silence_timeout: 1.0,
        speech_hints: hints,
        text: "How much is #{first_num} plus #{second_num}?"
      )
      
      if input.result.to_i == first_num + second_num
        is_spammer = false
        break
      else
        is_spammer =  true
        tries += 1
        remaining = max_tries - tries
        call.play_tts text: "That is wrong! You have #{remaining} more attempts" if (remaining) > 0
      end
    end

    if is_spammer
      @spammers << call.from
      handle_spammer(call)
    else
      @humans << call.from
      connect_human(call)
    end
  end

If the caller is confirmed as coming from a spammer, we will play a short message and hang the call up!

  def handle_spammer(call)
    call.play_tts text: "Not this time, spammer boy!"
    call.hangup
  end

If the caller is a human, we will forward the call to our regular call flow! You can add any To number to your .env file in order to complete this section.

  def connect_human(call)
    call.play_tts text: "Connecting you to my master"
    call_handler = call.connect [[{ type: 'phone', params: { to_number: ENV['TO_NUMBER'], from_number: call.from, timeout: 30 } }]]
    activate_detector(call, call_handler.call)
    call.hangup
  end

Lastly, we will define the most important function! When a call comes in, we will answer and begin recording. We will first check to see if the caller is a known spammer or human and if so, we will skip the CAPTCHA. If it is not recognized, we will call our handle_captcha function in order to make a determination, forward the call flow appropriately, and add the number to the correct array! When the call is complete, we will stop recording and log the recording URL.

def on_incoming_call(call)
    call.answer
    record_handle = call.record! direction: "both", initial_timeout: 10, end_silence_timeout: 0, stereo: true

    if known_spammer?(call)
      handle_spammer(call)
    elsif known_caller?(call)
      connect_human(call)
    else
      handle_captcha(call)
    end

    record_handle.stop
    logger.info("Recorded to #{record_handle.url}")
  end

All of the functions defined above to handle the call data, call flow, and CAPTCHA will run inside a Relay context like so.

class AssistantConsumer < Signalwire::Relay::Consumer
  contexts ['incoming']
# define functions here 
AssistantConsumer.new.run

Running the example

  • Create an .env file according to .env.example
  • Run bundle install
  • Run ruby assistant_consumer.rb
  • Set up a number on your SignalWire dashboard to call a Relay context named incoming
  • Dial your number and prepare to find out if you are a human!

As Seen on LIVEWire

If you want to see a live code breakdown, explanation, and demonstration of this guide at work, click here to watch it on YouTube! While you're there, feel free to take a look at our YouTube Channel to see other LIVEWire code and application breakdowns!

Sign Up Here

If you would like to test this example out, you can create a SignalWire account and space here.

Please feel free to reach out to us on our Community Slack or create a Support ticket if you need guidance!


What’s Next

Check out the full code on our SignalWire Github Repo!

Did this page help you?