lib/adhearsion/voip/asterisk/commands.rb in adhearsion-0.8.3 vs lib/adhearsion/voip/asterisk/commands.rb in adhearsion-0.8.4
- old
+ new
@@ -2,18 +2,18 @@
require 'adhearsion/voip/menu_state_machine/menu_class'
module Adhearsion
module VoIP
module Asterisk
- module Commands
-
+ module Commands
+
RESPONSE_PREFIX = "200 result=" unless defined? RESPONSE_PREFIX
-
+
# These are the status messages that asterisk will issue after a dial command is executed.
- #
+ #
# Here is a current list of dial status messages which are not all necessarily supported by adhearsion:
- #
+ #
# ANSWER: Call is answered. A successful dial. The caller reached the callee.
# BUSY: Busy signal. The dial command reached its number but the number is busy.
# NOANSWER: No answer. The dial command reached its number, the number rang for too long, then the dial timed out.
# CANCEL: Call is cancelled. The dial command reached its number but the caller hung up before the callee picked up.
# CONGESTION: Congestion. This status is usually a sign that the dialled number is not recognised.
@@ -22,93 +22,122 @@
# TORTURE: Privacy mode, callee chose to send caller to torture menu
# INVALIDARGS: Error parsing Dial command arguments (added for Asterisk 1.4.1, SVN r53135-53136)
#
# @see http://www.voip-info.org/wiki/index.php?page=Asterisk+variable+DIALSTATUS Asterisk Variable DIALSTATUS
DIAL_STATUSES = Hash.new(:unknown).merge(:answer => :answered, #:doc:
- :congestion => :congested,
+ :congestion => :congested,
:busy => :busy,
:cancel => :cancelled,
:noanswer => :unanswered,
:cancelled => :cancelled,
:chanunavail => :channel_unavailable) unless defined? DIAL_STATUSES
-
+
DYNAMIC_FEATURE_EXTENSIONS = {
:attended_transfer => lambda do |options|
variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
extend_dynamic_features_with "atxfer"
end,
:blind_transfer => lambda do
variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
extend_dynamic_features_with 'blindxfer'
end
} unless defined? DYNAMIC_FEATURE_EXTENSIONS
-
- # Utility method to write to pbx.
- def write(message)
+
+ # Utility method to write to pbx.
+ def write(message)
to_pbx.print(message)
end
-
- # Utility method to read from pbx. Hangup if nil.
- def read
+
+ # Utility method to read from pbx. Hangup if nil.
+ def read
returning from_pbx.gets do |message|
raise Hangup if message.nil?
+ raise Hangup if message.match(/^HANGUP\n?$/i)
+ raise Hangup if message.match(/^511 Command Not Permitted on a dead channel/i)
ahn_log.agi.debug "<<< #{message}"
end
end
-
+
# This method is the underlying method executed by nearly all the command methods in this module.
# It is used to send the plaintext commands in the proper AGI format over TCP/IP back to an Asterisk server via the
# FAGI protocol.
# It is not recommended that you call this method directly unless you plan to write a new command method
# in which case use this method you to communicate directly with an Asterisk server via the FAGI protocol.
# @see http://www.voip-info.org/wiki/view/Asterisk+FastAGI More information about FAGI
def raw_response(message = nil)
+ raise ArgumentError.new("illegal NUL in message #{message.inspect}") if message =~ /\0/
ahn_log.agi.debug ">>> #{message}"
write message if message
read
end
-
+
+ def response(command, *arguments)
+ # Arguments surrounded by quotes; quotes backslash-escaped.
+ # See parse_args in asterisk/res/res_agi.c (Asterisk 1.4.21.1)
+ quote_arg = lambda { |arg|
+ '"' + arg.gsub(/["\\]/) { |m| "\\#{m}" } + '"'
+ }
+ if arguments.empty?
+ raw_response("#{command}")
+ else
+ raw_response("#{command} " + arguments.map{ |arg| quote_arg.call(arg.to_s) }.join(' '))
+ end
+ end
+
# The answer command must be called first before any other commands can be issued.
# In typical adhearsion applications the answer command is called by default as soon
# as a call is transfered to a valid context in dialplan.rb.
# If you do not want your adhearsion application to automatically issue an answer command,
# then you must edit your startup.rb file and configure this setting.
- # Keep in mind that you should not need to issue another answer command after
+ # Keep in mind that you should not need to issue another answer command after
# an answer command has already been issued either explicitly by your code or implicitly
# by the standard adhearsion configuration.
def answer
- raw_response "ANSWER"
+ response "ANSWER"
true
end
-
+
# This asterisk dialplan command allows you to instruct Asterisk to start applications
# which are typically run from extensions.conf.
#
# The most common commands are already made available through the FAGI interface provided
# by this code base. For commands that do not fall into this category, then exec is what you
# should use.
#
- # For example, if there are specific asterisk modules you have loaded that will not
+ # For example, if there are specific asterisk modules you have loaded that will not
# available through the standard commands provided through FAGI - then you can used EXEC.
#
# @example Using execute in this way will add a header to an existing SIP call.
# execute 'SIPAddHeader', '"Call-Info: answer-after=0"
- #
+ #
# @see http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands Asterisk Dialplan Commands
def execute(application, *arguments)
- result = raw_response("EXEC #{application} #{arguments * '|'}")
+ result = raw_response(%{EXEC %s "%s"} % [ application,
+ arguments.join(%{"#{AHN_CONFIG.asterisk.argument_delimiter}"}) ])
return false if error?(result)
result
end
-
+
+ # Sends a message to the console via the verbose message system.
+ # @example Use this command to inform someone watching the Asterisk console
+ # of actions happening within Adhearsion.
+ # verbose 'Processing call with Adhearsion' 3
+ #
+ # @see http://www.voip-info.org/wiki/view/verbose
+ def verbose(message, level = nil)
+ result = raw_response("VERBOSE \"#{message}\" #{level}")
+ return false if error?(result)
+ result
+ end
+
# Hangs up the current channel. After this command is issued, you will not be able to send any more AGI
# commands but the dialplan Thread will still continue, allowing you to do any post-call work.
#
def hangup
- raw_response 'HANGUP'
+ response 'HANGUP'
end
-
+
# Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now),
# Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. When playing
# numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
# is pronounced as "one hundred" instead of "one zero zero".
#
@@ -116,15 +145,15 @@
# file encoded using the current channel's codec, if one exists. If not, it will transcode from
# the default codec (GSM). Asterisk stores its sound files in /var/lib/asterisk/sounds.
#
# @example Play file hello-world.???
# play 'hello-world'
- # @example Speak current time
+ # @example Speak current time
# play Time.now
- # @example Play sound file, speak number, play two more sound files
+ # @example Play sound file, speak number, play two more sound files
# play %w"a-connect-charge-of 22 cents-per-minute will-apply"
- # @example Play two sound files
+ # @example Play two sound files
# play "you-sound-cute", "what-are-you-wearing"
#
def play(*arguments)
arguments.flatten.each do |argument|
play_time(argument) || play_numeric(argument) || play_string(argument)
@@ -146,31 +175,46 @@
# record 'my-file.gsm', :silence => 5, :maxduration => 120
#
def record(*args)
options = args.last.kind_of?(Hash) ? args.pop : {}
filename = args.shift || "/tmp/recording_%d.gsm"
+ if (!options.has_key?(:format))
+ format = filename.slice!(/\.[^\.]+$/)
+ if (format.nil?)
+ ahn_log.agi.warn "Format not specified and not detected. Defaulting to \"gsm\""
+ format = gsm
+ end
+ format.sub!(/^\./, "")
+ else
+ format = options.delete(:format)
+ end
silence = options.delete(:silence) || 0
- maxduration = options.delete(:maxduration) || 0
+ maxduration = options.delete(:maxduration) || -1
+ escapedigits = options.delete(:escapedigits) || "#"
- execute("Record", filename, silence, maxduration)
+ if (silence > 0)
+ response("RECORD FILE", filename, format, escapedigits, maxduration,0, "BEEP", "s=#{silence}")
+ else
+ response("RECORD FILE", filename, format, escapedigits, maxduration, 0, "BEEP")
+ end
# If the user hangs up before the recording is entered, -1 is returned and RECORDED_FILE
# will not contain the name of the file, even though it IS in fact recorded.
- filename.index("%d") ? get_variable('RECORDED_FILE') : filename
+ filename.index("%d") ? get_variable('RECORDED_FILE') : filename + "." + format
end
-
+
# Simulates pressing the specified digits over the current channel. Can be used to
# traverse a phone menu.
def dtmf(digits)
execute "SendDTMF", digits.to_s
end
-
+
# The with_next_message method...
def with_next_message(&block)
raise LocalJumpError, "Must supply a block" unless block_given?
- block.call(next_message)
- end
+ block.call(next_message)
+ end
# This command should be used to advance to the next message in the Asterisk Comedian Voicemail application
def next_message
@call.inbox.pop
end
@@ -181,125 +225,125 @@
end
# Menu creates an interactive menu for the caller.
#
# The following documentation was derived from a post on Jay Phillips' blog (see below).
- #
+ #
# The menu() command solves the problem of building enormous input-fetching state machines in Ruby without first-class
# message passing facilities or an external DSL.
- #
+ #
# Here is an example dialplan which uses the menu() command effectively.
- #
+ #
# from_pstn {
# menu 'welcome', 'for-spanish-press-8', 'main-ivr',
# :timeout => 8.seconds, :tries => 3 do |link|
# link.shipment_status 1
# link.ordering 2
# link.representative 4
# link.spanish 8
# link.employee 900..999
- #
+ #
# link.on_invalid { play 'invalid' }
- #
+ #
# link.on_premature_timeout do |str|
# play 'sorry'
# end
- #
+ #
# link.on_failure do
# play 'goodbye'
# hangup
# end
# end
# }
- #
+ #
# shipment_status {
# # Fetch a tracking number and pass it to a web service.
# }
- #
+ #
# ordering {
# # Enter another menu that lets them enter credit card
# # information and place their order over the phone.
# }
- #
+ #
# representative {
# # Place the caller into a queue
# }
- #
+ #
# spanish {
# # Special options for the spanish menu.
# }
- #
+ #
# employee {
# dial "SIP/#{extension}" # Overly simplistic
# }
- #
+ #
# The main detail to note is the declarations within the menu() command’s block. Each line seems to refer to a link object
# executing a seemingly arbitrary method with an argument that’s either a number or a Range of numbers. The +link+ object
# collects these arbitrary method invocations and assembles a set of rules. The seemingly arbitrary method name is the name
# of the context to which the menu should jump in case its argument (the pattern) is found to be a match.
- #
+ #
# With these context names and patterns defined, the +menu()+ command plays in sequence the sound files you supply as
# arguments, stopping playback abruptly if the user enters a digit. If no digits were pressed when the files finish playing,
# it waits +:timeout+ seconds. If no digits are pressed after the timeout, it executes the +on_premature_timeout+ hook you
# define (if any) and then tries again a maximum of +:tries+ times. If digits are pressed that result in no possible match,
# it executes the +on_invalid+ hook. When/if all tries are exhausted with no positive match, it executes the +on_failure+
# hook after the other hook (e.g. +on_invalid+, then +on_failure+).
- #
+ #
# When the +menu()+ state machine runs through the defined rules, it must distinguish between exact and potential matches.
# It's important to understand the differences between these and how they affect the overall outcome:
- #
+ #
# |---------------|-------------------|------------------------------------------------------|
# | exact matches | potential matches | result |
# |---------------|-------------------|------------------------------------------------------|
# | 0 | 0 | Fail and start over |
# | 1 | 0 | Match found! |
# | 0 | >0 | Get another digit |
# | >1 | 0 | Go with the first exact match |
# | 1 | >0 | Get another digit. If timeout, use exact match |
# | >1 | >0 | Get another digit. If timeout, use first exact match |
# |---------------|-------------------|------------------------------------------------------|
- #
+ #
# == Database integration
- #
+ #
# To do database integration, I recommend programatically executing methods on the link object within the block. For example:
- #
+ #
# menu do |link|
# for employee in Employee.find(:all)
# link.internal employee.extension
# end
# end
- #
+ #
# or this more efficient and Rubyish way
- #
+ #
# menu do |link|
# link.internal *Employee.find(:all).map(&:extension)
# end
- #
+ #
# If this second example seems like too much Ruby magic, let me explain — +Employee.find(:all)+ effectively does a “SELECT *
# FROM employees” on the database with ActiveRecord, returning (what you’d think is) an Array. The +map(&:extension)+ is
# fanciness that means “replace every instance in this Array with the result of calling extension on that object”. Now we
# have an Array of every extension in the database. The splat operator (*) before the argument changes the argument from
# being one argument (an Array) into a sequence of n arguments, where n is the number of items in the Array it’s “splatting”.
# Lastly, these arguments are passed to the internal method, the name of a context which will handle dialing this user if one
# of the supplied patterns matches.
- #
+ #
# == Handling a successful pattern match
- #
+ #
# Which brings me to another important note. Let’s say that the user’s input successfully matched one of the patterns
# returned by that Employe.find... magic. When it jumps to the internal context, that context can access the variable entered
# through the extension variable. This was a tricky design decision that I think, overall, works great. It makes the +menu()+
# command feel much more first-class in the Adhearsion dialplan grammar and decouples the receiving context from the menu
# that caused the jump. After all, the context doesn’t necessary need to be the endpoint from a menu; it can be its own entry
# point, making menu() effectively a pipeline of re-creating the call.
- #
+ #
# @see http://jicksta.com/articles/2008/02/11/menu-command Original Blog Post
def menu(*args, &block)
options = args.last.kind_of?(Hash) ? args.pop : {}
sound_files = args.flatten
-
+
menu_instance = Menu.new(options, &block)
-
+
initial_digit_prompt = sound_files.any?
# This method is basically one big begin/rescue block. When we start the Menu state machine by continue()ing, the state
# machine will pass messages back to this method in the form of Exceptions. This decoupling allows the menu system to
# work on, say, Freeswitch and Asterisk both.
@@ -314,11 +358,11 @@
case result_of_menu
when Menu::MenuResultInvalid
menu_instance.execute_invalid_hook
menu_instance.restart!
when Menu::MenuGetAnotherDigit
-
+
next_digit = play_sound_files_for_menu(menu_instance, sound_files)
if next_digit
menu_instance << next_digit
else
# The user timed out entering another digit!
@@ -342,11 +386,11 @@
# Retry will re-execute the begin block, preserving our changes to the menu_instance object.
retry
end
end
-
+
# This method is used to receive keypad input from the user. Digits are collected
# via DTMF (keypad) input until one of three things happens:
#
# 1. The number of digits you specify as the first argument is collected
# 2. The timeout you specify with the :timeout option elapses.
@@ -362,11 +406,11 @@
# input 9, :timeout => 7, :accept_key => "0" # Receives nine digits, returning
# # when the timeout is encountered
# # or when the "0" key is pressed.
# input 3, :play => "you-sound-cute"
# input :play => ["if-this-is-correct-press", 1, "otherwise-press", 2]
- #
+ #
# When specifying files to play, the playback of the sequence of files will stop
# immediately when the user presses the first digit.
#
# The :timeout option works like a digit timeout, therefore each digit pressed
# causes the timer to reset. This is a much more user-friendly approach than an
@@ -376,28 +420,28 @@
# because there'd be no other way to end the collection of digits. You can
# obviously override this by passing in a new key with :accept_key.
def input(*args)
options = args.last.kind_of?(Hash) ? args.pop : {}
number_of_digits = args.shift
-
+
sound_files = Array options.delete(:play)
timeout = options.delete(:timeout)
terminating_key = options.delete(:accept_key)
terminating_key = if terminating_key
terminating_key.to_s
elsif number_of_digits.nil? && !terminating_key.equal?(false)
'#'
end
-
+
if number_of_digits && number_of_digits < 0
ahn_log.agi.warn "Giving -1 to input() is now deprecated. Don't specify a first " +
"argument to simulate unlimited digits." if number_of_digits == -1
raise ArgumentError, "The number of digits must be positive!"
end
-
+
buffer = ''
- key = sound_files.any? ? interruptable_play(*sound_files) || '' : wait_for_digit(timeout || -1)
+ key = sound_files.any? ? interruptible_play(*sound_files) || '' : wait_for_digit(timeout || -1)
loop do
return buffer if key.nil?
if terminating_key
if key == terminating_key
return buffer
@@ -410,11 +454,11 @@
return buffer if number_of_digits && number_of_digits == buffer.length
end
key = wait_for_digit(timeout || -1)
end
end
-
+
# Jumps to a context. An alternative to DialplanContextProc#+@. When jumping to a context, it will *not* resume executing
# the former context when the jumped-to context has finished executing. Make sure you don't have any
# +ensure+ closures which you expect to execute when the call has finished, as they will run when
# this method is called.
#
@@ -424,31 +468,31 @@
# variables here, you're effectively blowing away the old variables. If you need them for some reason,
# you should assign the important ones to an instance variable first before calling this method.
def jump_to(context, overrides={})
context = lookup_context_with_name(context) if context.kind_of?(Symbol) || (context.kind_of?(String) && context =~ /^[\w_]+$/)
raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException unless context.kind_of?(Adhearsion::DialPlan::DialplanContextProc)
-
+
if overrides.any?
overrides = overrides.symbolize_keys
if overrides.has_key?(:extension) && !overrides[:extension].kind_of?(Adhearsion::VoIP::DSL::PhoneNumber)
overrides[:extension] = Adhearsion::VoIP::DSL::PhoneNumber.new overrides[:extension]
end
-
+
overrides.each_pair do |key, value|
meta_def(key) { value }
end
end
-
+
raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context)
end
-
+
# The queue method puts a call into a call queue to be answered by an agent registered with that queue.
# The queue method takes a queue_name as an argument to place the caller in the appropriate queue.
- # @see http://www.voip-info.org/wiki-Asterisk+cmd+Queue Full information on the Asterisk Queue
+ # @see http://www.voip-info.org/wiki-Asterisk+cmd+Queue Full information on the Asterisk Queue
def queue(queue_name)
queue_name = queue_name.to_s
-
+
@queue_proxy_hash_lock = Mutex.new unless defined? @queue_proxy_hash_lock
@queue_proxy_hash_lock.synchronize do
@queue_proxy_hash ||= {}
if @queue_proxy_hash.has_key? queue_name
return @queue_proxy_hash[queue_name]
@@ -456,11 +500,11 @@
proxy = @queue_proxy_hash[queue_name] = QueueProxy.new(queue_name, self)
return proxy
end
end
end
-
+
# Returns the status of the last dial(). Possible dial
# statuses include :answer, :busy, :no_answer, :cancelled,
# :congested, and :channel_unavailable. If :cancel is
# returned, the caller hung up before the callee picked
# up. If :congestion is returned, the dialed extension
@@ -478,19 +522,19 @@
# Opposite of last_dial_successful?()
def last_dial_unsuccessful?
not last_dial_successful?
end
-
+
# This feature is presently experimental! Do not use it!
def speak(text, engine=:none)
- engine = Adhearsion::Configuration::AsteriskConfiguration.speech_engine || engine
+ engine = AHN_CONFIG.asterisk.speech_engine || engine
execute SpeechEngines.send(engine, text)
end
-
+
# This method is a high-level way of enabling features you create/uncomment from features.conf.
- #
+ #
# Certain Symbol features you enable (as defined in DYNAMIC_FEATURE_EXTENSIONS) have optional
# arguments that you can also specify here. The usage examples show how to do this.
#
# Usage examples:
#
@@ -500,33 +544,33 @@
# # sets "TRANSFER_CONTEXT" to :context's value
#
# enable_feature :blind_transfer, :context => 'my_dial' # Enables 'blindxfer' and sets TRANSFER_CONTEXT
#
# enable_feature "foobar" # Enables "foobar"
- #
+ #
# enable_feature("dup"); enable_feature("dup") # Enables "dup" only once.
def enable_feature(feature_name, optional_options=nil)
if DYNAMIC_FEATURE_EXTENSIONS.has_key? feature_name
instance_exec(optional_options, &DYNAMIC_FEATURE_EXTENSIONS[feature_name])
else
raise ArgumentError, "You cannot supply optional options when the feature name is " +
"not internally recognized!" if optional_options
extend_dynamic_features_with feature_name
end
end
-
+
# Disables a feature name specified in features.conf. If you're disabling it, it was probably
# set by enable_feature().
def disable_feature(feature_name)
enabled_features_variable = variable 'DYNAMIC_FEATURES'
enabled_features = enabled_features_variable.split('#')
if enabled_features.include? feature_name
enabled_features.delete feature_name
variable 'DYNAMIC_FEATURES' => enabled_features.join('#')
end
end
-
+
# Used to join a particular conference with the MeetMe application. To
# use MeetMe, be sure you have a proper timing device configured on your
# Asterisk box. MeetMe is Asterisk's built-in conferencing program.
# @see http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe Asterisk Meetme Application Information
def join(conference_id, options={})
@@ -534,38 +578,38 @@
command_flags = options[:options].to_s # This is a passthrough string straight to Asterisk
pin = options[:pin]
raise ArgumentError, "A conference PIN number must be numerical!" if pin && pin.to_s !~ /^\d+$/
# The 'd' option of MeetMe creates conferences dynamically.
command_flags += 'd' unless command_flags.include? 'd'
-
+
execute "MeetMe", conference_id, command_flags, options[:pin]
end
-
+
# Issue this command to access a channel variable that exists in the asterisk dialplan (i.e. extensions.conf)
# Use get_variable to pass information from other modules or high level configurations from the asterisk dialplan
# to the adhearsion dialplan.
# @see: http://www.voip-info.org/wiki/view/get+variable Asterisk Get Variable
def get_variable(variable_name)
- result = raw_response("GET VARIABLE #{variable_name}")
+ result = response("GET VARIABLE", variable_name)
case result
when "200 result=0"
return nil
- when /^200 result=1 \((.*)\)$/
- return $LAST_PAREN_MATCH
+ when /^200 result=1 \((.*)\)$/
+ return $LAST_PAREN_MATCH
end
end
-
+
# Use set_variable to pass information back to the asterisk dial plan.
# Keep in mind that the variables are not global variables. These variables only exist for the channel
# related to the call that is being serviced by the particular instance of your adhearsion application.
# You will not be able to pass information back to the asterisk dialplan for other instances of your adhearsion
# application to share. Once the channel is "hungup" then the variables are cleared and their information is gone.
# @see http://www.voip-info.org/wiki/view/set+variable Asterisk Set Variable
def set_variable(variable_name, value)
- raw_response("SET VARIABLE %s %p" % [variable_name.to_s, value.to_s]) == "200 result=1"
+ response("SET VARIABLE", variable_name, value) == "200 result=1"
end
-
+
# The variable method allows you to either set or get a channel variable from Asterisk
# The method takes a hash key/value pair if you would like to set a variable
# Or a single string with the variable to get from Asterisk
def variable(*args)
if args.last.kind_of? Hash
@@ -580,30 +624,30 @@
else
args.map { |var| get_variable(var) }
end
end
end
-
- # Use the voicemail method to send a caller to a voicemail box to leave a message.
+
+ # Use the voicemail method to send a caller to a voicemail box to leave a message.
# @see http://www.voip-info.org/tiki-index.php?page=Asterisk+cmd+VoiceMail Asterisk Voicemail
# The method takes the mailbox_number of the user to leave a message for and a
# greeting_option that will determine which message gets played to the caller.
def voicemail(*args)
options_hash = args.last.kind_of?(Hash) ? args.pop : {}
mailbox_number = args.shift
greeting_option = options_hash.delete(:greeting)
skip_option = options_hash.delete(:skip)
raise ArgumentError, 'You supplied too many arguments!' if mailbox_number && options_hash.any?
greeting_option = case greeting_option
- when :busy: 'b'
- when :unavailable: 'u'
- when nil: nil
+ when :busy then 'b'
+ when :unavailable then 'u'
+ when nil then nil
else raise ArgumentError, "Unrecognized greeting #{greeting_option}"
end
skip_option &&= 's'
options = "#{greeting_option}#{skip_option}"
-
+
raise ArgumentError, "Mailbox cannot be blank!" if !mailbox_number.nil? && mailbox_number.blank?
number_with_context = if mailbox_number then mailbox_number else
raise ArgumentError, "You must supply ONE context name!" if options_hash.size != 1
context_name, mailboxes = options_hash.to_a.first
Array(mailboxes).map do |mailbox|
@@ -611,57 +655,57 @@
"#{mailbox}@#{context_name}"
end.join('&')
end
execute('voicemail', number_with_context, options)
case variable('VMSTATUS')
- when 'SUCCESS': true
- when 'USEREXIT': false
+ when 'SUCCESS' then true
+ when 'USEREXIT' then false
else nil
end
end
-
+
# The voicemail_main method puts a caller into the voicemail system to fetch their voicemail
# or set options for their voicemail box.
# @see http://www.voip-info.org/wiki-Asterisk+cmd+VoiceMailMain Asterisk VoiceMailMain Command
def voicemail_main(options={})
mailbox, context, folder = options.values_at :mailbox, :context, :folder
authenticate = options.has_key?(:authenticate) ? options[:authenticate] : true
-
+
folder = if folder
if folder.to_s =~ /^[\w_]+$/
"a(#{folder})"
else
- raise ArgumentError, "Voicemail folder must be alphanumerical/underscore characters only!"
+ raise ArgumentError, "Voicemail folder must be alphanumerical/underscore characters only!"
end
elsif folder == ''
raise "Folder name cannot be an empty String!"
else
nil
end
-
+
real_mailbox = ""
real_mailbox << "#{mailbox}" unless mailbox.blank?
real_mailbox << "@#{context}" unless context.blank?
-
+
real_options = ""
real_options << "s" if !authenticate
real_options << folder unless folder.blank?
-
+
command_args = [real_mailbox]
command_args << real_options unless real_options.blank?
command_args.clear if command_args == [""]
-
+
execute 'VoiceMailMain', *command_args
end
-
+
def check_voicemail
ahn_log.agi.warn "THE check_voicemail() DIALPLAN METHOD WILL SOON BE DEPRECATED! CHANGE THIS TO voicemail_main() INSTEAD"
voicemail_main
end
-
+
# Use this command to dial an extension or "phone number" in asterisk.
- # This command maps to the Asterisk DIAL command in the asterisk dialplan.
+ # This command maps to the Asterisk DIAL command in the asterisk dialplan.
#
# The first parameter, number, must be a string that represents the extension or "number" that asterisk should dial.
# Be careful to not just specify a number like 5001, 9095551001
# You must specify a properly formatted string as Asterisk would expect to use in order to understand
# whether the call should be dialed using SIP, IAX, or some other means.
@@ -684,134 +728,134 @@
#
# +:confirm+ - ?
#
# @example Make a call to the PSTN using my SIP provider for VoIP termination
# dial("SIP/19095551001@my.sip.voip.terminator.us")
- #
+ #
# @example Make 3 Simulataneous calls to the SIP extensions separated by & symbols, try for 15 seconds and use the callerid
# for this call specified by the variable my_callerid
# dial "SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => my_callerid
#
- # @example Make a call using the IAX provider to the PSTN
+ # @example Make a call using the IAX provider to the PSTN
# dial("IAX2/my.id@voipjet/19095551234", :name=>"John Doe", :caller_id=>"9095551234")
#
# @see http://www.voip-info.org/wiki-Asterisk+cmd+Dial Asterisk Dial Command
def dial(number, options={})
*recognized_options = :caller_id, :name, :for, :options, :confirm
-
+
unrecognized_options = options.keys - recognized_options
raise ArgumentError, "Unknown dial options: #{unrecognized_options.to_sentence}" if unrecognized_options.any?
set_caller_id_name options[:name]
set_caller_id_number options[:caller_id]
confirm_option = dial_macro_option_compiler options[:confirm]
all_options = options[:options]
all_options = all_options ? all_options + confirm_option : confirm_option
execute "Dial", number, options[:for], all_options
end
-
-
+
+
# This implementation of dial() uses the experimental call routing DSL.
#
# def dial(number, options={})
# rules = callable_routes_for number
# return :no_route if rules.empty?
# call_attempt_status = nil
# rules.each do |provider|
- #
+ #
# response = execute "Dial",
# provider.format_number_for_platform(number),
# timeout_from_dial_options(options),
# asterisk_options_from_dial_options(options)
- #
+ #
# call_attempt_status = last_dial_status
# break if call_attempt_status == :answered
# end
# call_attempt_status
# end
-
-
+
+
# Speaks the digits given as an argument. For example, "123" is spoken as "one two three".
def say_digits(digits)
execute "saydigits", validate_digits(digits)
end
-
+
# Returns the number of seconds the given block takes to execute as a Float. This
# is particularly useful in dialplans for tracking billable time. Note that
# if the call is hung up during the block, you will need to rescue the
# exception if you have some mission-critical logic after it with which
# you're recording this return-value.
def duration_of
start_time = Time.now
yield
Time.now - start_time
end
-
+
#
# This will play a sequence of files, stopping the playback if a digit is pressed. If a digit is pressed, it will be
# returned as a String. If the files played with no keypad input, nil will be returned.
#
def interruptible_play(*files)
files.flatten.each do |file|
- result = result_digit_from raw_response("EXEC BACKGROUND #{file}")
+ result = result_digit_from response("STREAM FILE", file, "1234567890*#")
return result if result != 0.chr
end
nil
end
protected
-
+
# wait_for_digits waits for the input of digits based on the number of milliseconds
def wait_for_digit(timeout=-1)
timeout *= 1_000 if timeout != -1
- result = result_digit_from raw_response("WAIT FOR DIGIT #{timeout.to_i}")
+ result = result_digit_from response("WAIT FOR DIGIT", timeout.to_i)
(result == 0.chr) ? nil : result
end
-
+
##
# Deprecated name of interruptible_play(). This is a misspelling!
#
def interruptable_play(*files)
ahn_log.deprecation.warn 'Please change your code to use interruptible_play() instead. "interruptable" is a misspelling! interruptable_play() will work for now but will be deprecated in the future!'
interruptible_play(*files)
end
-
+
# set_callier_id_number method allows setting of the callerid number of the call
def set_caller_id_number(caller_id)
return unless caller_id
raise ArgumentError, "Caller ID must be numerical" if caller_id.to_s !~ /^\d+$/
- raw_response %(SET CALLERID %p) % caller_id
+ response "SET CALLERID", caller_id
end
-
+
# set_caller_id_name method allows the setting of the callerid name of the call
def set_caller_id_name(caller_id_name)
return unless caller_id_name
variable "CALLERID(name)" => caller_id_name
end
-
+
def timeout_from_dial_options(options)
options[:for] || options[:timeout]
end
-
+
def asterisk_options_from_dial_options(options)
# TODO: Will become much more sophisticated soon to handle callerid, etc
options[:options]
end
-
+
def dial_macro_option_compiler(confirm_argument_value)
defaults = { :macro => 'ahn_dial_confirmer',
:timeout => 20.seconds,
:play => "beep",
:key => '#' }
-
+
case confirm_argument_value
when true
DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument(defaults)
when false, nil
''
when Proc
raise NotImplementedError, "Coming in the future, you can do :confirm => my_context."
-
+
when Hash
options = defaults.merge confirm_argument_value
if((confirm_argument_value.keys - defaults.keys).any?)
raise ArgumentError, "Known options: #{defaults.keys.to_sentence}"
end
@@ -828,71 +872,71 @@
raise ArgumentError, "Unrecognized :timeout! #{options[:timeout].inspect}"
end
raise ArgumentError, "Unrecognized DTMF key: #{options[:key]}" unless options[:key].to_s =~ /^[\d#*]$/
options[:play] = Array(options[:play]).join('++')
DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument options
-
+
else
raise ArgumentError, "Unrecognized :confirm option: #{confirm_argument_value.inspect}!"
end
end
-
+
def result_digit_from(response_string)
raise ArgumentError, "Can't coerce nil into AGI response! This could be a bug!" unless response_string
digit = response_string[/^#{response_prefix}(-?\d+(\.\d+)?)/,1]
digit.to_i.chr if digit && digit.to_s != "-1"
end
-
+
def extract_input_from(result)
return false if error?(result)
# return false if input_timed_out?(result)
-
+
# This regexp doesn't match if there was a timeout with no
# inputted digits, therefore returning nil.
-
- result[/^#{response_prefix}([\d*]+)/, 1]
+
+ result[/^#{response_prefix}([\d*]+)/, 1]
end
-
+
def extract_variable_from(result)
return false if error?(result)
result[/^#{response_prefix}1 \((.+)\)/, 1]
end
-
+
def get_dial_status
dial_status = variable('DIALSTATUS')
dial_status ? dial_status.downcase.to_sym : :cancelled
end
def play_time(argument)
if argument.kind_of? Time
execute(:sayunixtime, argument.to_i)
end
end
-
+
def play_numeric(argument)
if argument.kind_of?(Numeric) || argument =~ /^\d+$/
execute(:saynumber, argument)
end
end
-
+
def play_string(argument)
execute(:playback, argument)
end
def play_sound_files_for_menu(menu_instance, sound_files)
digit = nil
if sound_files.any? && menu_instance.digit_buffer_empty?
- digit = interruptable_play(*sound_files)
+ digit = interruptible_play(*sound_files)
end
digit || wait_for_digit(menu_instance.timeout)
end
-
+
def extend_dynamic_features_with(feature_name)
current_variable = variable("DYNAMIC_FEATURES") || ''
enabled_features = current_variable.split '#'
unless enabled_features.include? feature_name
- enabled_features << feature_name
+ enabled_features << feature_name
variable "DYNAMIC_FEATURES" => enabled_features.join('#')
end
end
def jump_to_context_with_name(context_name)
@@ -914,50 +958,50 @@
end
def to_pbx
io
end
-
+
def from_pbx
io
end
-
+
def validate_digits(digits)
returning digits.to_s do |digits_as_string|
raise ArgumentError, "Can only be called with valid digits!" unless digits_as_string =~ /^[0-9*#-]+$/
end
end
-
+
def error?(result)
result.to_s[/^#{response_prefix}(?:-\d+|0)/]
end
-
+
# timeout with pressed digits: 200 result=<digits> (timeout)
# timeout without pressed digits: 200 result= (timeout)
# @see http://www.voip-info.org/wiki/view/get+data AGI Get Data
def input_timed_out?(result)
result.starts_with?(response_prefix) && result.ends_with?('(timeout)')
end
-
+
def io
call.io
end
-
+
def response_prefix
RESPONSE_PREFIX
end
-
+
class QueueProxy
-
+
class << self
-
+
def format_join_hash_key_arguments(options)
-
+
bad_argument = lambda do |(key, value)|
raise ArgumentError, "Unrecognize value for #{key.inspect} -- #{value.inspect}"
end
-
+
# Direct Queue() arguments:
timeout = options.delete :timeout
announcement = options.delete :announce
# Terse single-character options
@@ -966,229 +1010,229 @@
allow_transfer = options.delete :allow_transfer
raise ArgumentError, "Unrecognized args to join!: #{options.inspect}" if options.any?
ring_style = case ring_style
- when :ringing: 'r'
- when :music: ''
+ when :ringing then 'r'
+ when :music then ''
when nil
else bad_argument[:play => ring_style]
end.to_s
-
+
allow_hangup = case allow_hangup
- when :caller: 'H'
- when :agent: 'h'
- when :everyone: 'Hh'
+ when :caller then 'H'
+ when :agent then 'h'
+ when :everyone then 'Hh'
when nil
else bad_argument[:allow_hangup => allow_hangup]
end.to_s
-
+
allow_transfer = case allow_transfer
- when :caller: 'T'
- when :agent: 't'
- when :everyone: 'Tt'
+ when :caller then 'T'
+ when :agent then 't'
+ when :everyone then 'Tt'
when nil
else bad_argument[:allow_transfer => allow_transfer]
end.to_s
-
+
terse_character_options = ring_style + allow_transfer + allow_hangup
-
+
[terse_character_options, '', announcement, timeout].map(&:to_s)
end
end
-
+
attr_reader :name, :environment
def initialize(name, environment)
@name, @environment = name, environment
end
-
+
# Makes the current channel join the queue. Below are explanations of the recognized Hash-key
# arguments supported by this method.
#
# :timeout - The number of seconds to wait for an agent to answer
# :play - Can be :ringing or :music.
# :announce - A sound file to play instead of the normal queue announcement.
# :allow_transfer - Can be :caller, :agent, or :everyone. Allow someone to transfer the call.
# :allow_hangup - Can be :caller, :agent, or :everyone. Allow someone to hangup with the * key.
- #
+ #
# Usage examples:
- #
+ #
# - queue('sales').join!
# - queue('sales').join! :timeout => 1.minute
# - queue('sales').join! :play => :music
# - queue('sales').join! :play => :ringing
# - queue('sales').join! :announce => "custom/special-queue-announcement"
# - queue('sales').join! :allow_transfer => :caller
# - queue('sales').join! :allow_transfer => :agent
# - queue('sales').join! :allow_hangup => :caller
# - queue('sales').join! :allow_hangup => :agent
# - queue('sales').join! :allow_hangup => :everyone
- # - queue('sales').join! :allow_transfer => :agent, :timeout => 30.seconds,
+ # - queue('sales').join! :allow_transfer => :agent, :timeout => 30.seconds,
def join!(options={})
environment.execute("queue", name, *self.class.format_join_hash_key_arguments(options))
normalize_queue_status_variable environment.variable("QUEUESTATUS")
end
-
+
def agents(options={})
cached = options.has_key?(:cache) ? options.delete(:cache) : true
raise ArgumentError, "Unrecognized arguments to agents(): #{options.inspect}" if options.keys.any?
if cached
@cached_proxy ||= QueueAgentsListProxy.new(self, true)
else
@uncached_proxy ||= QueueAgentsListProxy.new(self, false)
end
end
-
+
def waiting_count
raise QueueDoesNotExistError.new(name) unless exists?
environment.variable("QUEUE_WAITING_COUNT(#{name})").to_i
end
-
+
def empty?
waiting_count == 0
end
-
+
def any?
waiting_count > 0
end
-
+
def exists?
environment.execute('RemoveQueueMember', name, 'SIP/AdhearsionQueueExistenceCheck')
environment.variable("RQMSTATUS") != 'NOSUCHQUEUE'
end
-
+
private
-
+
def normalize_queue_status_variable(variable)
returning variable.downcase.to_sym do |queue_status|
raise QueueDoesNotExistError.new(name) if queue_status == :unknown
end
end
-
+
class QueueAgentsListProxy
-
+
include Enumerable
-
+
attr_reader :proxy, :agents
def initialize(proxy, cached=false)
@proxy = proxy
@cached = cached
end
-
+
def count
if cached? && @cached_count
@cached_count
else
@cached_count = proxy.environment.variable("QUEUE_MEMBER_COUNT(#{proxy.name})").to_i
end
end
alias size count
alias length count
-
+
# Supported Hash-key arguments are :penalty and :name. The :name value will be viewable in
# the queue_log. The :penalty is the penalty assigned to this agent for answering calls on
# this queue
def new(*args)
-
+
options = args.last.kind_of?(Hash) ? args.pop : {}
interface = args.shift || ''
-
+
raise ArgumentError, "You may only supply an interface and a Hash argument!" if args.any?
-
+
penalty = options.delete(:penalty) || ''
name = options.delete(:name) || ''
-
+
raise ArgumentError, "Unrecognized argument(s): #{options.inspect}" if options.any?
-
+
proxy.environment.execute("AddQueueMember", proxy.name, interface, penalty, '', name)
-
+
case proxy.environment.variable("AQMSTATUS")
- when "ADDED" : true
- when "MEMBERALREADY" : false
- when "NOSUCHQUEUE" : raise QueueDoesNotExistError.new(proxy.name)
+ when "ADDED" then true
+ when "MEMBERALREADY" then false
+ when "NOSUCHQUEUE" then raise QueueDoesNotExistError.new(proxy.name)
else
raise "UNRECOGNIZED AQMSTATUS VALUE!"
end
-
+
# TODO: THIS SHOULD RETURN AN AGENT INSTANCE
end
-
+
# Logs a pre-defined agent into this queue and waits for calls. Pass in :silent => true to stop
# the message which says "Agent logged in".
def login!(*args)
options = args.last.kind_of?(Hash) ? args.pop : {}
-
+
silent = options.delete(:silent).equal?(false) ? '' : 's'
id = args.shift
id &&= AgentProxy.id_from_agent_channel(id)
raise ArgumentError, "Unrecognized Hash options to login(): #{options.inspect}" if options.any?
raise ArgumentError, "Unrecognized argument to login(): #{args.inspect}" if args.any?
-
+
proxy.environment.execute('AgentLogin', id, silent)
end
-
+
# Removes the current channel from this queue
def logout!
# TODO: DRY this up. Repeated in the AgentProxy...
proxy.environment.execute 'RemoveQueueMember', proxy.name
case proxy.environment.variable("RQMSTATUS")
- when "REMOVED" : true
- when "NOTINQUEUE" : false
+ when "REMOVED" then true
+ when "NOTINQUEUE" then false
when "NOSUCHQUEUE"
raise QueueDoesNotExistError.new(proxy.name)
else
raise "Unrecognized RQMSTATUS variable!"
end
end
-
+
def each(&block)
check_agent_cache!
agents.each(&block)
end
-
+
def first
check_agent_cache!
agents.first
end
-
+
def last
check_agent_cache!
agents.last
end
-
+
def cached?
@cached
end
-
+
def to_a
check_agent_cache!
@agents
end
-
+
private
-
+
def check_agent_cache!
if cached?
load_agents! unless agents
else
load_agents!
end
end
-
+
def load_agents!
raw_data = proxy.environment.variable "QUEUE_MEMBER_LIST(#{proxy.name})"
@agents = raw_data.split(',').map(&:strip).reject(&:empty?).map do |agent|
AgentProxy.new(agent, proxy)
end
@cached_count = @agents.size
end
-
+
end
-
+
class AgentProxy
-
+
SUPPORTED_METADATA_NAMES = %w[status password name mohclass exten channel] unless defined? SUPPORTED_METADATA_NAMES
class << self
def id_from_agent_channel(id)
id = id.to_s
@@ -1201,116 +1245,116 @@
@interface = interface
@id = self.class.id_from_agent_channel interface
@proxy = proxy
@queue_name = proxy.name
end
-
+
def remove!
proxy.environment.execute 'RemoveQueueMember', queue_name, interface
case proxy.environment.variable("RQMSTATUS")
- when "REMOVED" : true
- when "NOTINQUEUE" : false
+ when "REMOVED" then true
+ when "NOTINQUEUE" then false
when "NOSUCHQUEUE"
raise QueueDoesNotExistError.new(queue_name)
else
raise "Unrecognized RQMSTATUS variable!"
end
end
-
+
# Pauses the given agent for this queue only. If you wish to pause this agent
# for all queues, pass in :everywhere => true. Returns true if the agent was
# successfully paused and false if the agent was not found.
def pause!(options={})
everywhere = options.delete(:everywhere)
args = [(everywhere ? nil : queue_name), interface]
proxy.environment.execute('PauseQueueMember', *args)
case proxy.environment.variable("PQMSTATUS")
- when "PAUSED" : true
- when "NOTFOUND" : false
+ when "PAUSED" then true
+ when "NOTFOUND" then false
else
raise "Unrecognized PQMSTATUS value!"
end
end
-
+
# Pauses the given agent for this queue only. If you wish to pause this agent
# for all queues, pass in :everywhere => true. Returns true if the agent was
# successfully paused and false if the agent was not found.
def unpause!(options={})
everywhere = options.delete(:everywhere)
args = [(everywhere ? nil : queue_name), interface]
proxy.environment.execute('UnpauseQueueMember', *args)
case proxy.environment.variable("UPQMSTATUS")
- when "UNPAUSED" : true
- when "NOTFOUND" : false
+ when "UNPAUSED" then true
+ when "NOTFOUND" then false
else
raise "Unrecognized UPQMSTATUS value!"
end
end
-
+
# Returns true/false depending on whether this agent is logged in.
def logged_in?
status == 'LOGGEDIN'
end
-
+
private
-
+
def status
agent_metadata 'status'
end
-
+
def agent_metadata(data_name)
data_name = data_name.to_s.downcase
raise ArgumentError, "unrecognized agent metadata name #{data_name}" unless SUPPORTED_METADATA_NAMES.include? data_name
proxy.environment.variable "AGENT(#{id}:#{data_name})"
end
-
+
end
-
+
class QueueDoesNotExistError < Exception
def initialize(queue_name)
super "Queue #{queue_name} does not exist!"
end
end
-
+
end
-
+
module MenuDigitResponse
def timed_out?
eql? 0.chr
end
end
module SpeechEngines
-
+
class InvalidSpeechEngine < Exception; end
-
+
class << self
def cepstral(text)
puts "in ceptral"
puts escape(text)
end
-
+
def festival(text)
raise NotImplementedError
end
-
+
def none(text)
raise InvalidSpeechEngine, "No speech engine selected. You must specify one in your Adhearsion config file."
end
-
+
def method_missing(engine_name, text)
raise InvalidSpeechEngine, "Unsupported speech engine #{engine_name} for speaking '#{text}'"
end
-
+
private
-
+
def escape(text)
"%p" % text
end
-
+
end
end
-
+
end
end
end
end