Last active
December 12, 2015 12:39
-
-
Save andrius/4773822 to your computer and use it in GitHub Desktop.
Questionnaire application, includes database and Asterisk AGI (from line 100). For new caller, app request to enter an PIN code to authenticate. App monitors how many times caller was returned to the system and how many attempts did to answer question.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/env ruby | |
# This application was made and tested in production with Ruby 1.8x | |
require 'rubygems' | |
require 'mysql' | |
require 'active_record' | |
require 'AGIServer' | |
require 'AGIMenu' | |
require 'AGISelection' | |
require 'daemons' | |
APP_ROOT = File.expand_path(File.dirname(__FILE__)) | |
# Questionnaire / voting is statis and here we have a list of correct answers | |
CORRECT_ANSWERS = %w(2 3 2 1 3 1 1 1 1 3) | |
# database credentials | |
DATABASE = { :adapter => :mysql, | |
:encoding => :utf8, | |
:database => 'voip', | |
:pool => 250, | |
:connections => 12, | |
:username => 'voip', | |
:password => 'PASSWORD', | |
#:socket => '/var/run/mysqld/mysqld.pid', | |
:host => '127.0.0.1', | |
:reconnect => true | |
} | |
ActiveRecord::Base.establish_connection DATABASE | |
logger = Logger.new(STDERR) | |
logger.level = Logger::DEBUG | |
ActiveRecord::Base.logger = logger | |
ActiveRecord::Base.colorize_logging = true | |
# create AR classes and DB tables | |
class VoteQuestion < ActiveRecord::Base | |
set_primary_key :question_number | |
end | |
class Questionnaire < ActiveRecord::Base; end | |
# create DB tables if not exists (dirty way!) | |
if ! VoteQuestion.table_exists? | |
ActiveRecord::Schema.define do | |
create_table :questionnairies do |table| | |
table.string :pin_code, :limit => 8, :null => false | |
end | |
end | |
#(1..10).each do |pin_code| | |
File.read("#{APP_ROOT}/codes.txt").chomp.split("\n").each do |pin_code| | |
Questionnaire.create( :pin_code => "00000000#{pin_code.chomp}"[-8,8] ) | |
end | |
ActiveRecord::Schema.define do | |
create_table :phone_numbers do |table| | |
table.integer :questionnaire_id, :default => nil | |
table.integer :number, :limit => 8, :unsigned => true, :null => false | |
table.integer :question_number, :null => false, :default => 0 | |
table.integer :count_of_calls_made, :null => false, :default => 0 | |
table.integer :amount_of_asked_questions, :null => false, :default => 0 | |
table.integer :amount_of_correct_answers, :null => false, :default => 0 | |
table.integer :total_amount_of_asked_questions, :null => false, :default => 0 | |
table.integer :total_amount_of_correct_answers, :null => false, :default => 0 | |
table.boolean :sms_sent, :null => false, :default => false | |
table.timestamps | |
end | |
add_index :phone_numbers, :number | |
end | |
ActiveRecord::Schema.define do | |
create_table :vote_questions, :primary_key => :question_number do |table| | |
table.integer :amount_of_answers, :null => false, :default => 3 | |
table.integer :correct_answer, :null => false | |
end | |
end | |
(1..10).each do |question_no| | |
VoteQuestion.create( | |
:question_number => question_no, | |
:amount_of_answers => 3, | |
:correct_answer => CORRECT_ANSWERS[question_no-1].to_i | |
) | |
end | |
end | |
class PhoneNumber < ActiveRecord::Base | |
validates_uniqueness_of :number | |
end | |
class CallAttempt < ActiveRecord::Base | |
belongs_to :phone_number | |
end | |
# AGI classes | |
class Farmacy < AGIRoute | |
# this method will be called from asteris dialplan (AGI calling) for every new inbound phone call | |
def vote | |
begin | |
agi.answer | |
AGIMenu.sounds_dir = AGISelection.sounds_dir = APP_ROOT + '/' | |
# prepare the IVR menu | |
hash = {:introduction=>["welcome", "instructions"], | |
:conclusion=>"what-is-your-choice", | |
:timeout=>17, | |
:choices=> | |
[{:dtmf=>"*", :audio=>["to-go-back", "press", "digits/star"]}, | |
{:dtmf=>1, :audio=>["press", "digits/1", "for-option-1"]}, | |
{:dtmf=>2, :audio=>["press", "digits/2", "for-option-2"]}, | |
{:dtmf=>"#", :audio=>["or", "press", "digits/pound", "to-repeat"]}]} | |
# fix callerid and locate caller | |
e164 = agi.channel_params['callerid'].to_i | |
number = PhoneNumber.find_by_number(e164) | |
# increase calls counter for existiing in DB phone number | |
unless number.nil? | |
number.count_of_calls_made += 1 | |
number.save | |
end | |
# create a new DB record for non-existing in DB phone number | |
phone_number = if number.nil? | |
pn = PhoneNumber.create :number => e164, :count_of_calls_made => 1 | |
# say 'welcome' or 'welcome back' & save state of caller | |
pn.questionnaire_id = hello_new_caller() | |
pn.save | |
pn | |
# if caller is not assigned to the specific Questionnaire, ... | |
elsif number.questionnaire_id.nil? | |
# say 'welcome' or 'welcome back' & save state of caller | |
number.questionnaire_id = hello_new_caller() | |
number.save | |
number | |
else | |
welcome_back number.question_number == 0 ? 'Hello_repeat_nopin' : 'Hello_repeat' | |
number | |
end | |
agi.exec 'Playback silence/1' | |
ask_question :phone_number => phone_number | |
thanks! :phone_number => phone_number | |
rescue => err | |
puts ">>>>>> ERROR: ", err.backtrace | |
end | |
end | |
private | |
def play_ivr(ivr) | |
begin | |
menu = AGIMenu.new(ivr) | |
result = menu.play(:agi => agi) | |
result | |
rescue => err | |
puts ">>>>>> ERROR: ", err.backtrace | |
nil | |
end | |
end | |
def playback(audio, timeout = 0.5) | |
play_ivr :timeout => timeout, :introduction => audio, | |
:choices => [ | |
{:dtmf => 1}, {:dtmf => 2}, {:dtmf => 3}, | |
{:dtmf => 4}, {:dtmf => 5}, {:dtmf => 6}, | |
{:dtmf => 7}, {:dtmf => 8}, {:dtmf => 9}, | |
{:dtmf=>'*'}, {:dtmf => 0}, {:dtmf=>'#'} ] | |
end | |
def press_one_to_confirm(return_value=nil) | |
code = 1.upto(3) do |count| | |
selection = playback('Wright', 3).to_s | |
break return_value if selection.to_s == '1' | |
if count == 3 then | |
agi.exec "Busy 12" | |
agi.exec "Hangup" | |
break nil | |
end | |
end | |
end | |
def hello_new_caller | |
begin | |
result = 1.upto(3) do |count| | |
selection = if count == 1 then | |
AGISelection.new :audio => 'Hello', :max_digits => 8 | |
else | |
AGISelection.new :audio => 'Wrong', :max_digits => 8 | |
end | |
code = selection.read(:agi => agi) | |
questionnaire = Questionnaire.find_by_pin_code(code.to_s) | |
break questionnaire unless questionnaire.nil? | |
end | |
if result.class == Questionnaire | |
press_one_to_confirm result.id | |
else | |
agi.exec "Busy 12" | |
agi.exec "Hangup" | |
nil | |
end | |
rescue => err | |
puts ">>>>>> ERROR: ", err.backtrace | |
end | |
end | |
def welcome_back(message='Hello_repeat') | |
playback message | |
press_one_to_confirm | |
end | |
def ask_question(params={}) | |
begin | |
phone_number = params[:phone_number] | |
question_number = phone_number.question_number + 1 | |
question_data = VoteQuestion.find_by_question_number(question_number) | |
if question_data.nil? then # finalize work - no more questions | |
return | |
end | |
ivr_data = { :introduction => question_number, :choices => [ ], :timeout => 12 } | |
(1..(question_data.amount_of_answers)).each do |possible_answer| | |
ivr_data[:choices].push({ | |
:dtmf => possible_answer, | |
:audio => "#{question_number}-#{possible_answer}" | |
}) | |
end | |
ivr_result = play_ivr ivr_data | |
phone_number.question_number = question_number | |
phone_number.amount_of_asked_questions += 1 | |
phone_number.total_amount_of_asked_questions += 1 | |
if question_data.correct_answer == ivr_result.to_i | |
phone_number.amount_of_correct_answers += 1 | |
phone_number.total_amount_of_correct_answers += 1 | |
end | |
phone_number.save | |
ask_question :phone_number => phone_number | |
rescue => err | |
puts err, err.backtrace | |
end | |
end | |
def thanks!(params={}) | |
phone_number = params[:phone_number] | |
if phone_number.amount_of_correct_answers == VoteQuestion.count then | |
playback 'Goodbye' | |
else | |
phone_number.question_number = 0 | |
phone_number.amount_of_asked_questions = 0 | |
phone_number.amount_of_correct_answers = 0 | |
phone_number.save | |
playback 'Goodbye_wrong' | |
end | |
end | |
end | |
trap('INT') { AGIServer.shutdown } | |
trap('TERM') { AGIServer.shutdown } | |
begin | |
config = { :bind_port => 4574, | |
:min_workers => 5, | |
:max_workers => 12, | |
:jobs_per_worker => 2, | |
:stats => false, | |
:bind_host => '127.0.0.1', | |
:logger => logger | |
} | |
AgiServer = AGIServer.new config | |
rescue Errno::EADDRINUSE | |
error = "Cannot start AGI Server, address already in use." | |
logger.fatal(error) | |
print "#{error}\n" | |
exit | |
else | |
print "#{$$}" | |
end | |
AgiServer.start | |
AgiServer.finish |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment