adhearsion 1.2.6 → 2.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -10
- data/CHANGELOG.md +273 -0
- data/Gemfile +1 -1
- data/Guardfile +17 -0
- data/README.markdown +61 -9
- data/Rakefile +16 -48
- data/adhearsion.gemspec +21 -7
- data/bin/ahn +3 -1
- data/cucumber.yml +4 -0
- data/features/app_generator.feature +42 -0
- data/features/cli.feature +108 -0
- data/features/step_definitions/app_generator_steps.rb +6 -0
- data/features/step_definitions/cli_steps.rb +74 -0
- data/features/support/aruba_helper.rb +22 -0
- data/features/support/env.rb +37 -0
- data/features/support/utils.rb +8 -0
- data/lib/adhearsion.rb +85 -41
- data/lib/adhearsion/call.rb +176 -0
- data/lib/adhearsion/call_controller.rb +134 -0
- data/lib/adhearsion/call_controller/dial.rb +70 -0
- data/lib/adhearsion/call_controller/input.rb +173 -0
- data/lib/adhearsion/call_controller/menu.rb +124 -0
- data/lib/adhearsion/call_controller/output.rb +267 -0
- data/lib/adhearsion/call_controller/record.rb +42 -0
- data/lib/adhearsion/call_controller/utility.rb +60 -0
- data/lib/adhearsion/calls.rb +81 -0
- data/lib/adhearsion/cli.rb +1 -3
- data/lib/adhearsion/cli_commands.rb +142 -0
- data/lib/adhearsion/configuration.rb +149 -0
- data/lib/adhearsion/console.rb +19 -8
- data/lib/adhearsion/dialplan_controller.rb +9 -0
- data/lib/adhearsion/events.rb +84 -0
- data/lib/adhearsion/foundation/all.rb +0 -7
- data/lib/adhearsion/foundation/custom_daemonizer.rb +4 -6
- data/lib/adhearsion/foundation/exception_handler.rb +9 -0
- data/lib/adhearsion/foundation/object.rb +26 -8
- data/lib/adhearsion/foundation/synchronized_hash.rb +3 -6
- data/lib/adhearsion/foundation/thread_safety.rb +17 -1
- data/lib/adhearsion/generators/app/app_generator.rb +4 -13
- data/lib/adhearsion/generators/app/templates/Gemfile +10 -5
- data/lib/adhearsion/generators/app/templates/Procfile +1 -0
- data/lib/adhearsion/generators/app/templates/README.md +28 -0
- data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +41 -0
- data/lib/adhearsion/generators/app/templates/{components/simon_game → lib}/simon_game.rb +6 -18
- data/lib/adhearsion/generators/app/templates/script/ahn +2 -2
- data/lib/adhearsion/initializer.rb +151 -293
- data/lib/adhearsion/initializer/logging.rb +33 -0
- data/lib/adhearsion/logging.rb +65 -69
- data/lib/adhearsion/menu_dsl.rb +15 -0
- data/lib/adhearsion/menu_dsl/calculated_match.rb +39 -0
- data/lib/adhearsion/menu_dsl/calculated_match_collection.rb +41 -0
- data/lib/adhearsion/menu_dsl/fixnum_match_calculator.rb +18 -0
- data/lib/adhearsion/menu_dsl/match_calculator.rb +36 -0
- data/lib/adhearsion/{voip/menu_state_machine/menu_class.rb → menu_dsl/menu.rb} +38 -40
- data/lib/adhearsion/menu_dsl/menu_builder.rb +69 -0
- data/lib/adhearsion/menu_dsl/range_match_calculator.rb +55 -0
- data/lib/adhearsion/menu_dsl/string_match_calculator.rb +21 -0
- data/lib/adhearsion/outbound_call.rb +64 -0
- data/lib/adhearsion/plugin.rb +319 -0
- data/lib/adhearsion/plugin/collection.rb +19 -0
- data/lib/adhearsion/plugin/initializer.rb +37 -0
- data/lib/adhearsion/plugin/methods_container.rb +6 -0
- data/lib/adhearsion/process.rb +94 -0
- data/lib/adhearsion/punchblock_plugin.rb +29 -0
- data/lib/adhearsion/punchblock_plugin/initializer.rb +137 -0
- data/lib/adhearsion/router.rb +30 -0
- data/lib/adhearsion/router/route.rb +42 -0
- data/lib/adhearsion/script_ahn_loader.rb +2 -2
- data/lib/adhearsion/tasks.rb +14 -9
- data/lib/adhearsion/tasks/configuration.rb +26 -0
- data/lib/adhearsion/tasks/plugins.rb +17 -0
- data/lib/adhearsion/version.rb +8 -14
- data/spec/adhearsion/call_controller/dial_spec.rb +138 -0
- data/spec/adhearsion/call_controller/input_spec.rb +278 -0
- data/spec/adhearsion/call_controller/menu_spec.rb +120 -0
- data/spec/adhearsion/call_controller/output_spec.rb +466 -0
- data/spec/adhearsion/call_controller/record_spec.rb +125 -0
- data/spec/adhearsion/call_controller_spec.rb +395 -0
- data/spec/adhearsion/call_spec.rb +438 -0
- data/spec/adhearsion/calls_spec.rb +47 -0
- data/spec/adhearsion/configuration_spec.rb +308 -0
- data/spec/adhearsion/dialplan_controller_spec.rb +26 -0
- data/spec/adhearsion/events_spec.rb +112 -0
- data/spec/adhearsion/initializer/logging_spec.rb +58 -0
- data/spec/adhearsion/initializer_spec.rb +209 -122
- data/spec/adhearsion/logging_spec.rb +58 -47
- data/spec/adhearsion/menu_dsl/calculated_match_collection_spec.rb +56 -0
- data/spec/adhearsion/menu_dsl/calculated_match_spec.rb +57 -0
- data/spec/adhearsion/menu_dsl/fixnum_match_calculator_spec.rb +33 -0
- data/spec/adhearsion/menu_dsl/match_calculator_spec.rb +13 -0
- data/spec/adhearsion/menu_dsl/menu_builder_spec.rb +118 -0
- data/spec/adhearsion/menu_dsl/menu_spec.rb +210 -0
- data/spec/adhearsion/menu_dsl/range_match_calculator_spec.rb +28 -0
- data/spec/adhearsion/menu_dsl/string_match_calculator_spec.rb +36 -0
- data/spec/adhearsion/menu_dsl_spec.rb +12 -0
- data/spec/adhearsion/outbound_call_spec.rb +174 -0
- data/spec/adhearsion/plugin_spec.rb +489 -0
- data/spec/adhearsion/process_spec.rb +34 -0
- data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +294 -0
- data/spec/adhearsion/router/route_spec.rb +99 -0
- data/spec/adhearsion/router_spec.rb +106 -0
- data/spec/adhearsion_spec.rb +46 -0
- data/spec/spec_helper.rb +14 -14
- data/spec/support/call_controller_test_helpers.rb +48 -0
- data/spec/support/initializer_stubs.rb +8 -13
- data/spec/support/punchblock_mocks.rb +6 -0
- metadata +255 -253
- data/CHANGELOG +0 -174
- data/bin/ahnctl +0 -68
- data/bin/jahn +0 -43
- data/examples/asterisk_manager_interface/standalone.rb +0 -51
- data/lib/adhearsion/commands.rb +0 -302
- data/lib/adhearsion/component_manager.rb +0 -278
- data/lib/adhearsion/component_manager/component_tester.rb +0 -54
- data/lib/adhearsion/component_manager/spec_framework.rb +0 -18
- data/lib/adhearsion/events_support.rb +0 -65
- data/lib/adhearsion/foundation/blank_slate.rb +0 -3
- data/lib/adhearsion/foundation/event_socket.rb +0 -205
- data/lib/adhearsion/foundation/future_resource.rb +0 -36
- data/lib/adhearsion/foundation/metaprogramming.rb +0 -17
- data/lib/adhearsion/foundation/numeric.rb +0 -13
- data/lib/adhearsion/foundation/pseudo_guid.rb +0 -10
- data/lib/adhearsion/foundation/relationship_properties.rb +0 -42
- data/lib/adhearsion/foundation/string.rb +0 -26
- data/lib/adhearsion/generators/app/templates/.ahnrc +0 -34
- data/lib/adhearsion/generators/app/templates/README +0 -8
- data/lib/adhearsion/generators/app/templates/components/ami_remote/ami_remote.rb +0 -15
- data/lib/adhearsion/generators/app/templates/components/disabled/HOW_TO_ENABLE +0 -7
- data/lib/adhearsion/generators/app/templates/components/disabled/stomp_gateway/README.markdown +0 -47
- data/lib/adhearsion/generators/app/templates/components/disabled/stomp_gateway/stomp_gateway.rb +0 -34
- data/lib/adhearsion/generators/app/templates/components/disabled/stomp_gateway/stomp_gateway.yml +0 -12
- data/lib/adhearsion/generators/app/templates/components/disabled/xmpp_gateway/README.markdown +0 -3
- data/lib/adhearsion/generators/app/templates/components/disabled/xmpp_gateway/xmpp_gateway.rb +0 -11
- data/lib/adhearsion/generators/app/templates/components/disabled/xmpp_gateway/xmpp_gateway.yml +0 -0
- data/lib/adhearsion/generators/app/templates/config/startup.rb +0 -81
- data/lib/adhearsion/generators/app/templates/dialplan.rb +0 -3
- data/lib/adhearsion/generators/app/templates/events.rb +0 -33
- data/lib/adhearsion/host_definitions.rb +0 -67
- data/lib/adhearsion/initializer/asterisk.rb +0 -86
- data/lib/adhearsion/initializer/configuration.rb +0 -324
- data/lib/adhearsion/initializer/database.rb +0 -60
- data/lib/adhearsion/initializer/drb.rb +0 -31
- data/lib/adhearsion/initializer/freeswitch.rb +0 -22
- data/lib/adhearsion/initializer/ldap.rb +0 -57
- data/lib/adhearsion/initializer/rails.rb +0 -41
- data/lib/adhearsion/initializer/xmpp.rb +0 -42
- data/lib/adhearsion/tasks/components.rb +0 -32
- data/lib/adhearsion/tasks/database.rb +0 -5
- data/lib/adhearsion/tasks/deprecations.rb +0 -59
- data/lib/adhearsion/tasks/generating.rb +0 -20
- data/lib/adhearsion/tasks/lint.rb +0 -4
- data/lib/adhearsion/voip/asterisk.rb +0 -4
- data/lib/adhearsion/voip/asterisk/agi_server.rb +0 -121
- data/lib/adhearsion/voip/asterisk/commands.rb +0 -1966
- data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +0 -140
- data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +0 -102
- data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +0 -250
- data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +0 -240
- data/lib/adhearsion/voip/asterisk/config_manager.rb +0 -64
- data/lib/adhearsion/voip/asterisk/manager_interface.rb +0 -697
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +0 -1681
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +0 -341
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +0 -78
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +0 -87
- data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +0 -80
- data/lib/adhearsion/voip/call.rb +0 -521
- data/lib/adhearsion/voip/call_routing.rb +0 -64
- data/lib/adhearsion/voip/commands.rb +0 -17
- data/lib/adhearsion/voip/constants.rb +0 -39
- data/lib/adhearsion/voip/conveniences.rb +0 -18
- data/lib/adhearsion/voip/dial_plan.rb +0 -252
- data/lib/adhearsion/voip/dsl/dialing_dsl.rb +0 -151
- data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +0 -37
- data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +0 -27
- data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +0 -124
- data/lib/adhearsion/voip/dsl/dialplan/parser.rb +0 -69
- data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +0 -16
- data/lib/adhearsion/voip/dsl/numerical_string.rb +0 -128
- data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +0 -48
- data/lib/adhearsion/voip/freeswitch/event_handler.rb +0 -58
- data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +0 -129
- data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +0 -38
- data/lib/adhearsion/voip/freeswitch/oes_server.rb +0 -195
- data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +0 -80
- data/lib/adhearsion/voip/menu_state_machine/matchers.rb +0 -123
- data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +0 -57
- data/lib/adhearsion/xmpp/connection.rb +0 -61
- data/lib/theatre.rb +0 -147
- data/lib/theatre/README.markdown +0 -64
- data/lib/theatre/callback_definition_loader.rb +0 -86
- data/lib/theatre/guid.rb +0 -23
- data/lib/theatre/invocation.rb +0 -131
- data/lib/theatre/namespace_manager.rb +0 -153
- data/lib/theatre/version.rb +0 -2
- data/spec/adhearsion/cli_spec.rb +0 -306
- data/spec/adhearsion/component_manager_spec.rb +0 -292
- data/spec/adhearsion/constants_spec.rb +0 -8
- data/spec/adhearsion/drb_spec.rb +0 -65
- data/spec/adhearsion/fixtures/dialplan.rb +0 -3
- data/spec/adhearsion/foundation/event_socket_spec.rb +0 -168
- data/spec/adhearsion/host_definitions_spec.rb +0 -79
- data/spec/adhearsion/initializer/configuration_spec.rb +0 -291
- data/spec/adhearsion/initializer/loading_spec.rb +0 -154
- data/spec/adhearsion/initializer/paths_spec.rb +0 -74
- data/spec/adhearsion/relationship_properties_spec.rb +0 -54
- data/spec/adhearsion/voip/asterisk/agi_server_spec.rb +0 -473
- data/spec/adhearsion/voip/asterisk/ami/ami_spec.rb +0 -550
- data/spec/adhearsion/voip/asterisk/ami/lexer/ami_fixtures.yml +0 -30
- data/spec/adhearsion/voip/asterisk/ami/lexer/lexer_story +0 -291
- data/spec/adhearsion/voip/asterisk/ami/lexer/lexer_story.rb +0 -241
- data/spec/adhearsion/voip/asterisk/ami/lexer/story_helper.rb +0 -124
- data/spec/adhearsion/voip/asterisk/commands_spec.rb +0 -3241
- data/spec/adhearsion/voip/asterisk/config_file_generators/agents_spec.rb +0 -251
- data/spec/adhearsion/voip/asterisk/config_file_generators/queues_spec.rb +0 -323
- data/spec/adhearsion/voip/asterisk/config_file_generators/voicemail_spec.rb +0 -306
- data/spec/adhearsion/voip/asterisk/config_manager_spec.rb +0 -127
- data/spec/adhearsion/voip/asterisk/menu_command/calculated_match_spec.rb +0 -109
- data/spec/adhearsion/voip/asterisk/menu_command/matchers_spec.rb +0 -97
- data/spec/adhearsion/voip/call_routing_spec.rb +0 -125
- data/spec/adhearsion/voip/dialplan_manager_spec.rb +0 -468
- data/spec/adhearsion/voip/dsl/dialing_dsl_spec.rb +0 -270
- data/spec/adhearsion/voip/dsl/dispatcher_spec.rb +0 -82
- data/spec/adhearsion/voip/dsl/dispatcher_spec_helper.rb +0 -45
- data/spec/adhearsion/voip/dsl/parser_spec.rb +0 -69
- data/spec/adhearsion/voip/freeswitch/basic_connection_manager_spec.rb +0 -39
- data/spec/adhearsion/voip/freeswitch/inbound_connection_manager_spec.rb +0 -39
- data/spec/adhearsion/voip/freeswitch/oes_server_spec.rb +0 -9
- data/spec/adhearsion/voip/numerical_string_spec.rb +0 -61
- data/spec/adhearsion/voip/phone_number_spec.rb +0 -45
- data/spec/support/the_following_code.rb +0 -3
- data/spec/theatre/dsl_examples/simple_before_call.rb +0 -7
- data/spec/theatre/dsl_spec.rb +0 -69
- data/spec/theatre/invocation_spec.rb +0 -182
- data/spec/theatre/namespace_spec.rb +0 -125
- data/spec/theatre/spec_helper_spec.rb +0 -28
- data/spec/theatre/theatre_class_spec.rb +0 -148
|
@@ -1,1966 +0,0 @@
|
|
|
1
|
-
require 'adhearsion/voip/menu_state_machine/menu_class'
|
|
2
|
-
require 'json'
|
|
3
|
-
|
|
4
|
-
module Adhearsion
|
|
5
|
-
module VoIP
|
|
6
|
-
module Asterisk
|
|
7
|
-
class AGIProtocolError < StandardError; end
|
|
8
|
-
|
|
9
|
-
module Commands
|
|
10
|
-
|
|
11
|
-
RESPONSE_PREFIX = "200 result=" unless defined? RESPONSE_PREFIX
|
|
12
|
-
AGI_SUCCESSFUL_RESPONSE = RESPONSE_PREFIX + "1"
|
|
13
|
-
|
|
14
|
-
# These are the status messages that asterisk will issue after a dial command is executed.
|
|
15
|
-
#
|
|
16
|
-
# Here is a current list of dial status messages which are not all necessarily supported by adhearsion:
|
|
17
|
-
#
|
|
18
|
-
# ANSWER: Call is answered. A successful dial. The caller reached the callee.
|
|
19
|
-
# BUSY: Busy signal. The dial command reached its number but the number is busy.
|
|
20
|
-
# NOANSWER: No answer. The dial command reached its number, the number rang for too long, then the dial timed out.
|
|
21
|
-
# CANCEL: Call is cancelled. The dial command reached its number but the caller hung up before the callee picked up.
|
|
22
|
-
# CONGESTION: Congestion. This status is usually a sign that the dialled number is not recognised.
|
|
23
|
-
# CHANUNAVAIL: Channel unavailable. On SIP, peer may not be registered.
|
|
24
|
-
# DONTCALL: Privacy mode, callee rejected the call
|
|
25
|
-
# TORTURE: Privacy mode, callee chose to send caller to torture menu
|
|
26
|
-
# INVALIDARGS: Error parsing Dial command arguments (added for Asterisk 1.4.1, SVN r53135-53136)
|
|
27
|
-
#
|
|
28
|
-
# @see http://www.voip-info.org/wiki/index.php?page=Asterisk+variable+DIALSTATUS Asterisk Variable DIALSTATUS
|
|
29
|
-
DIAL_STATUSES = Hash.new(:unknown).merge(:answer => :answered, #:doc:
|
|
30
|
-
:congestion => :congested,
|
|
31
|
-
:busy => :busy,
|
|
32
|
-
:cancel => :cancelled,
|
|
33
|
-
:noanswer => :unanswered,
|
|
34
|
-
:cancelled => :cancelled,
|
|
35
|
-
:chanunavail => :channel_unavailable) unless defined? DIAL_STATUSES
|
|
36
|
-
|
|
37
|
-
DYNAMIC_FEATURE_EXTENSIONS = {
|
|
38
|
-
:attended_transfer => lambda do |options|
|
|
39
|
-
variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
|
|
40
|
-
extend_dynamic_features_with "atxfer"
|
|
41
|
-
end,
|
|
42
|
-
:blind_transfer => lambda do |options|
|
|
43
|
-
variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
|
|
44
|
-
extend_dynamic_features_with 'blindxfer'
|
|
45
|
-
end
|
|
46
|
-
} unless defined? DYNAMIC_FEATURE_EXTENSIONS
|
|
47
|
-
|
|
48
|
-
PLAYBACK_SUCCESS = 'SUCCESS' unless defined? PLAYBACK_SUCCESS
|
|
49
|
-
|
|
50
|
-
# Utility method to write to pbx.
|
|
51
|
-
# @param [String] message raw message
|
|
52
|
-
def write(message)
|
|
53
|
-
to_pbx.print message + "\n"
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Utility method to read from pbx. Hangup if nil.
|
|
57
|
-
def read
|
|
58
|
-
begin
|
|
59
|
-
from_pbx.gets.tap do |message|
|
|
60
|
-
# AGI has many conditions that might indicate a hangup
|
|
61
|
-
raise Hangup if message.nil?
|
|
62
|
-
|
|
63
|
-
ahn_log.agi.debug "<<< #{message}"
|
|
64
|
-
|
|
65
|
-
code, rest = *message.split(' ', 2)
|
|
66
|
-
|
|
67
|
-
case code.to_i
|
|
68
|
-
when 510
|
|
69
|
-
# This error is non-fatal for the call
|
|
70
|
-
ahn_log.agi.warn "510: Invalid or unknown AGI command"
|
|
71
|
-
when 511
|
|
72
|
-
# 511 Command Not Permitted on a dead channel
|
|
73
|
-
ahn_log.agi.debug "511: Dead channel. Raising Hangup"
|
|
74
|
-
raise Hangup
|
|
75
|
-
when 520
|
|
76
|
-
# This error is non-fatal for the call
|
|
77
|
-
ahn_log.agi.warn "520: Invalid command syntax"
|
|
78
|
-
when (500..599)
|
|
79
|
-
# Assume this error is non-fatal for the call and try to keep running
|
|
80
|
-
ahn_log.agi.warn "#{code}: Unknown AGI protocol error."
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# If the message starts with HANGUP it's a silly 1.6 OOB message
|
|
84
|
-
case message
|
|
85
|
-
when /^HANGUP/, /^HANGUP\n?$/i, /^HANGUP\s?\d{3}/i
|
|
86
|
-
ahn_log.agi.debug "AGI HANGUP. Raising hangup"
|
|
87
|
-
raise Hangup
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
rescue Errno::ECONNRESET
|
|
91
|
-
raise Hangup
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# The underlying method executed by nearly all the command methods in this module.
|
|
96
|
-
# Used to send the plaintext commands in the proper AGI format over TCP/IP back to an Asterisk server via the
|
|
97
|
-
# FAGI protocol.
|
|
98
|
-
#
|
|
99
|
-
# It is not recommended that you call this method directly unless you plan to write a new command method
|
|
100
|
-
# in which case use this to communicate directly with an Asterisk server via the FAGI protocol.
|
|
101
|
-
#
|
|
102
|
-
# @param [String] message
|
|
103
|
-
#
|
|
104
|
-
# @see http://www.voip-info.org/wiki/view/Asterisk+FastAGI More information about FAGI
|
|
105
|
-
def raw_response(message = nil)
|
|
106
|
-
message.squish!
|
|
107
|
-
@call.with_command_lock do
|
|
108
|
-
raise ArgumentError.new("illegal NUL in message #{message.inspect}") if message =~ /\0/
|
|
109
|
-
ahn_log.agi.debug ">>> #{message}"
|
|
110
|
-
write message if message
|
|
111
|
-
read
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def response(command, *arguments)
|
|
116
|
-
if arguments.empty?
|
|
117
|
-
raw_response("#{command}")
|
|
118
|
-
else
|
|
119
|
-
raw_response("#{command} " + arguments.map{ |arg| quote_arg(arg) }.join(' '))
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Arguments surrounded by quotes; quotes backslash-escaped.
|
|
124
|
-
# See parse_args in asterisk/res/res_agi.c (Asterisk 1.4.21.1)
|
|
125
|
-
def quote_arg(arg)
|
|
126
|
-
'"' + arg.to_s.gsub(/["\\]/) { |m| "\\#{m}" } + '"'
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Parses a response in the form of "200 result=some_value"
|
|
130
|
-
def inline_return_value(result)
|
|
131
|
-
return nil unless result
|
|
132
|
-
case result.chomp
|
|
133
|
-
when "200 result=0" then nil
|
|
134
|
-
when /^200 result=(.*)$/ then $LAST_PAREN_MATCH
|
|
135
|
-
else raise AGIProtocolError, "Invalid AGI response: #{result}"
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Parses a response in the form of "200 result=0 (some_value)"
|
|
140
|
-
def inline_result_with_return_value(result)
|
|
141
|
-
return nil unless result
|
|
142
|
-
case result.chomp
|
|
143
|
-
when "200 result=0" then nil
|
|
144
|
-
when /^#{AGI_SUCCESSFUL_RESPONSE} \((.*)\)$/ then $LAST_PAREN_MATCH
|
|
145
|
-
else raise AGIProtocolError, "Invalid AGI response: #{result}"
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# This must be called first before any other commands can be issued.
|
|
151
|
-
# In typical Adhearsion applications this is called by default as soon as a call is
|
|
152
|
-
# transfered to a valid context in dialplan.rb.
|
|
153
|
-
# If you do not want your Adhearsion application to automatically issue an answer command,
|
|
154
|
-
# then you must edit your startup.rb file and configure this setting.
|
|
155
|
-
# Keep in mind that you should not need to issue another answer command after one has already
|
|
156
|
-
# been issued either explicitly by your code or implicitly by the standard adhearsion configuration.
|
|
157
|
-
def answer
|
|
158
|
-
response "ANSWER"
|
|
159
|
-
true
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# This asterisk dialplan command allows you to instruct Asterisk to start applications
|
|
163
|
-
# which are typically run from extensions.conf.
|
|
164
|
-
#
|
|
165
|
-
# The most common commands are already made available through the FAGI interface provided
|
|
166
|
-
# by this code base. For commands that do not fall into this category, then exec is what you
|
|
167
|
-
# should use.
|
|
168
|
-
#
|
|
169
|
-
# For example, if there are specific asterisk modules you have loaded that will not be
|
|
170
|
-
# available through the standard commands provided through FAGI - then you can used EXEC.
|
|
171
|
-
#
|
|
172
|
-
# @example Using execute in this way will add a header to an existing SIP call.
|
|
173
|
-
# execute 'SIPAddHeader', '"Call-Info: answer-after=0"
|
|
174
|
-
#
|
|
175
|
-
# @see http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands Asterisk Dialplan Commands
|
|
176
|
-
def execute(application, *arguments)
|
|
177
|
-
command = "EXEC #{application}"
|
|
178
|
-
arguments = arguments.map { |arg| quote_arg(arg) }.join(AHN_CONFIG.asterisk.argument_delimiter)
|
|
179
|
-
result = raw_response("#{command} #{arguments}")
|
|
180
|
-
return false if error?(result)
|
|
181
|
-
result
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Sends a message to the console via the verbose message system.
|
|
185
|
-
#
|
|
186
|
-
# @param [String] message
|
|
187
|
-
# @param [Integer] level
|
|
188
|
-
#
|
|
189
|
-
# @return the result of the command
|
|
190
|
-
#
|
|
191
|
-
# @example Use this command to inform someone watching the Asterisk console
|
|
192
|
-
# of actions happening within Adhearsion.
|
|
193
|
-
# verbose 'Processing call with Adhearsion' 3
|
|
194
|
-
#
|
|
195
|
-
# @see http://www.voip-info.org/wiki/view/verbose
|
|
196
|
-
def verbose(message, level = nil)
|
|
197
|
-
result = response('VERBOSE', message, level)
|
|
198
|
-
return false if error?(result)
|
|
199
|
-
result
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# Hangs up the current channel. After this command is issued, you will not be able to send any more AGI
|
|
203
|
-
# commands but the dialplan Thread will still continue, allowing you to do any post-call work.
|
|
204
|
-
#
|
|
205
|
-
def hangup
|
|
206
|
-
response 'HANGUP'
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now),
|
|
210
|
-
# Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. When playing
|
|
211
|
-
# numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
|
|
212
|
-
# is pronounced as "one hundred" instead of "one zero zero". To specify how the Date/Time objects are said
|
|
213
|
-
# pass in as an array with the first parameter as the Date/Time/DateTime object along with a hash with the
|
|
214
|
-
# additional options. See play_time for more information.
|
|
215
|
-
#
|
|
216
|
-
# Note: it is not necessary to supply a sound file extension; Asterisk will try to find a sound
|
|
217
|
-
# file encoded using the current channel's codec, if one exists. If not, it will transcode from
|
|
218
|
-
# the default codec (GSM). Asterisk stores its sound files in /var/lib/asterisk/sounds.
|
|
219
|
-
#
|
|
220
|
-
# @example Play file hello-world.???
|
|
221
|
-
# play 'hello-world'
|
|
222
|
-
# @example Speak current time
|
|
223
|
-
# play Time.now
|
|
224
|
-
# @example Speak today's date
|
|
225
|
-
# play Date.today
|
|
226
|
-
# @example Speak today's date in a specific format
|
|
227
|
-
# play [Date.today, {:format => 'BdY'}]
|
|
228
|
-
# @example Play sound file, speak number, play two more sound files
|
|
229
|
-
# play %w"a-connect-charge-of 22 cents-per-minute will-apply"
|
|
230
|
-
# @example Play two sound files
|
|
231
|
-
# play "you-sound-cute", "what-are-you-wearing"
|
|
232
|
-
#
|
|
233
|
-
# @return [Boolean] true is returned if everything was successful. Otherwise, false indicates that
|
|
234
|
-
# some sound file(s) could not be played.
|
|
235
|
-
def play(*arguments)
|
|
236
|
-
result = true
|
|
237
|
-
unless play_time(arguments)
|
|
238
|
-
arguments.flatten.each do |argument|
|
|
239
|
-
# result starts off as true. But if the following command ever returns false, then result
|
|
240
|
-
# remains false.
|
|
241
|
-
result &= play_numeric(argument) || play_soundfile(argument)
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
result
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# Same as {#play}, but immediately raises an exception if a sound file cannot be played.
|
|
248
|
-
#
|
|
249
|
-
# @return [true]
|
|
250
|
-
# @raise [Adhearsion::VoIP::PlaybackError] If a sound file cannot be played
|
|
251
|
-
def play!(*arguments)
|
|
252
|
-
unless play_time(arguments)
|
|
253
|
-
arguments.flatten.each do |argument|
|
|
254
|
-
play_numeric(argument) || play_soundfile!(argument)
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
true
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
# Attempts to play a sound prompt. If the prompt is unplayable, for
|
|
261
|
-
# example, if the file is not present, then attempt to speak the prompt
|
|
262
|
-
# using Text-To-Speech.
|
|
263
|
-
#
|
|
264
|
-
# @param [Hash] Map of prompts and fallback TTS options
|
|
265
|
-
# @return [true]
|
|
266
|
-
# @raise [ArgumentError] If prompt cannot be found and TTS text is not specified
|
|
267
|
-
#
|
|
268
|
-
# @example Play "tt-monkeys" or say "Ooh ooh eee eee eee"
|
|
269
|
-
# play_or_speak 'tt-monkeys' => {:text => "Ooh ooh eee eee eee"}
|
|
270
|
-
#
|
|
271
|
-
# @example Play "pbx-invalid" or say "I'm sorry, that is not a valid extension. Please try again." and allowing the user to interrupt the TTS with "#"
|
|
272
|
-
# play_or_speak 'pbx-invalid' => {:text => "I'm sorry, that is not a valid extension. Please try again", :engine => :unimrcp}
|
|
273
|
-
def play_or_speak(prompts)
|
|
274
|
-
interrupted = nil
|
|
275
|
-
unless interrupted
|
|
276
|
-
prompts.each do |filename, options|
|
|
277
|
-
if filename && !filename.empty?
|
|
278
|
-
begin
|
|
279
|
-
if options[:interruptible]
|
|
280
|
-
interrupted = interruptible_play! filename
|
|
281
|
-
else
|
|
282
|
-
play! filename
|
|
283
|
-
end
|
|
284
|
-
rescue PlaybackError
|
|
285
|
-
raise ArgumentError, "Must supply TTS text as fallback" unless options[:text]
|
|
286
|
-
interrupted = speak options.delete(:text), options
|
|
287
|
-
end
|
|
288
|
-
else
|
|
289
|
-
interrupted = speak options.delete(:text), options
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
interrupted
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
# Records a sound file with the given name. If no filename is specified a file named by Asterisk
|
|
297
|
-
# will be created and returned. Else the given filename will be returned. If a relative path is
|
|
298
|
-
# given, the file will be saved in the default Asterisk sound directory, /var/lib/spool/asterisk
|
|
299
|
-
# by default.
|
|
300
|
-
#
|
|
301
|
-
# @param [string] file name to record to. Full path information is optional. If you want to change the
|
|
302
|
-
# format of the file you will want to add a .<valid extention> to the end of the file name specifying the
|
|
303
|
-
# filetype you want to record in. Alternately you can pass it is as :format in the options
|
|
304
|
-
#
|
|
305
|
-
# @param [hash] options
|
|
306
|
-
#
|
|
307
|
-
# +:silence+ - silence in seconds
|
|
308
|
-
#
|
|
309
|
-
# +:maxduration+ - maximum duration in seconds
|
|
310
|
-
#
|
|
311
|
-
# +:escapedigits+ - digits to be used to excape from recording
|
|
312
|
-
#
|
|
313
|
-
# +:beep+ - soundfile to use as a beep before recording. if not specifed defaults to system generated beep, set to nil for no beep.
|
|
314
|
-
#
|
|
315
|
-
# +:format+ - the format of the file to be recorded
|
|
316
|
-
#
|
|
317
|
-
# Silence and maxduration is specified in seconds.
|
|
318
|
-
#
|
|
319
|
-
# @return [String] The filename of the recorded file.
|
|
320
|
-
#
|
|
321
|
-
# @example Asterisk generated filename
|
|
322
|
-
# filename = record
|
|
323
|
-
# @example Specified filename
|
|
324
|
-
# record '/path/to/my-file.gsm'
|
|
325
|
-
# @example All options specified
|
|
326
|
-
# record 'my-file.gsm', :silence => 5, :maxduration => 120
|
|
327
|
-
#
|
|
328
|
-
# @deprecated please use {#record_to_file} instead
|
|
329
|
-
def record(*args)
|
|
330
|
-
options = args.last.kind_of?(Hash) ? args.pop : {}
|
|
331
|
-
filename = args.shift || "/tmp/recording_%d"
|
|
332
|
-
filename = increment_filename_counter filename
|
|
333
|
-
|
|
334
|
-
# Work around Ruby 1.8 limitation with *args
|
|
335
|
-
newargs = []
|
|
336
|
-
newargs << filename
|
|
337
|
-
newargs += args
|
|
338
|
-
newargs << options
|
|
339
|
-
record_to_file *newargs
|
|
340
|
-
|
|
341
|
-
# Reconstruct the logic that took place in #base_record_to_file, since it does not return the
|
|
342
|
-
# constructed filename
|
|
343
|
-
if (options.has_key?(:format))
|
|
344
|
-
format = options[:format]
|
|
345
|
-
else
|
|
346
|
-
filename, format = format_from_filename filename
|
|
347
|
-
end
|
|
348
|
-
"#{filename}.#{format}"
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
# Records a sound file with the given name. If no filename is specified a file named by Asterisk
|
|
352
|
-
# will be created and returned. Else the given filename will be returned. If a relative path is
|
|
353
|
-
# given, the file will be saved in the default Asterisk sound directory, /var/lib/spool/asterisk
|
|
354
|
-
# by default.
|
|
355
|
-
#
|
|
356
|
-
# @param [string] file name to record to. Full path information is optional. If you want to change the
|
|
357
|
-
# format of the file you will want to add a .<valid extention> to the end of the file name specifying the
|
|
358
|
-
# filetype you want to record in. If you don't specify a valid extension it will default to gsm and a
|
|
359
|
-
# .gsm will be added to the file. If you don't specify a filename it will write one in /tmp/recording_%d
|
|
360
|
-
# with %d being a counter that increments from 0 onward for the particular call you are making.
|
|
361
|
-
#
|
|
362
|
-
# @param [hash] options
|
|
363
|
-
#
|
|
364
|
-
# +:silence+ - silence in seconds
|
|
365
|
-
#
|
|
366
|
-
# +:maxduration+ - maximum duration in seconds
|
|
367
|
-
#
|
|
368
|
-
# +:escapedigits+ - digits to be used to excape from recording
|
|
369
|
-
#
|
|
370
|
-
# +:beep+ - soundfile to use as a beep before recording. if not specifed defaults to system generated beep, set to nil for no beep.
|
|
371
|
-
#
|
|
372
|
-
# +:format+ - the format of the file to be recorded. This will over-ride a implicit format in a file extension and append a .<format> to the end of the file.
|
|
373
|
-
#
|
|
374
|
-
# Silence and maxduration is specified in seconds.
|
|
375
|
-
#
|
|
376
|
-
# @return [Symbol] One of the follwing..... :hangup, :write_error, :success_dtmf, :success_timeout
|
|
377
|
-
#
|
|
378
|
-
# A sound file will be recorded to the specifed file unless a :write_error is returned. A :success_dtmf is
|
|
379
|
-
# for when a call was ended with a DTMF tone. A :success_timeout is returned when a call times out due to
|
|
380
|
-
# a silence longer than the specified silence or if the recording reaches the maxduration.
|
|
381
|
-
#
|
|
382
|
-
# @example Asterisk generated filename
|
|
383
|
-
# filename = record
|
|
384
|
-
# @example Specified filename
|
|
385
|
-
# record '/path/to/my-file.gsm'
|
|
386
|
-
# @example All options specified
|
|
387
|
-
# record 'my-file.gsm', :silence => 5, :maxduration => 120
|
|
388
|
-
#
|
|
389
|
-
def record_to_file(*args)
|
|
390
|
-
base_record_to_file(*args).last
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
# This works the same record_to_file except is throws an exception if a playback or write error occurs.
|
|
394
|
-
#
|
|
395
|
-
def record_to_file!(*args)
|
|
396
|
-
# raise PlaybackError, "Playback failed with PLAYBACKSTATUS: #{playback.inspect}. The raw response was #{response.inspect}."
|
|
397
|
-
return_values = base_record_to_file(*args)
|
|
398
|
-
if return_values.first == :playback_error
|
|
399
|
-
raise PlaybackError, "Playback failed with PLAYBACKSTATUS: #{return_values.second.inspect}."
|
|
400
|
-
elsif return_values.first == :write_error
|
|
401
|
-
raise RecordError, "Record failed on write."
|
|
402
|
-
end
|
|
403
|
-
return_values.first
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
def format_from_filename(filename)
|
|
407
|
-
filename, format = filename.match(/(^.*)\.([^\.]+)$/).captures rescue [filename, nil]
|
|
408
|
-
if (format.nil?)
|
|
409
|
-
ahn_log.agi.warn "Format not specified and not detected. Defaulting to \"gsm\""
|
|
410
|
-
format = "gsm"
|
|
411
|
-
end
|
|
412
|
-
[filename, format]
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
def increment_filename_counter(filename)
|
|
416
|
-
if filename.index("%d")
|
|
417
|
-
if @call.variables.has_key?(:recording_counter)
|
|
418
|
-
@call.variables[:recording_counter] += 1
|
|
419
|
-
else
|
|
420
|
-
@call.variables[:recording_counter] = 0
|
|
421
|
-
end
|
|
422
|
-
filename = filename % @call.variables[:recording_counter]
|
|
423
|
-
end
|
|
424
|
-
filename
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
# this is a base methor record_to_file and record_to_file! and should only be used via those methods
|
|
428
|
-
#
|
|
429
|
-
def base_record_to_file(*args)
|
|
430
|
-
options = args.last.kind_of?(Hash) ? args.pop : {}
|
|
431
|
-
filename = args.shift || "/tmp/recording_#{new_guid}_%d"
|
|
432
|
-
|
|
433
|
-
filename = increment_filename_counter filename
|
|
434
|
-
|
|
435
|
-
if (options.has_key?(:format))
|
|
436
|
-
format = options[:format]
|
|
437
|
-
else
|
|
438
|
-
filename, format = format_from_filename filename
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
# maxduration must be in milliseconds when using RECORD FILE
|
|
442
|
-
maxduration = options.delete(:maxduration) || -1
|
|
443
|
-
maxduration = maxduration * 1000 if maxduration > 0
|
|
444
|
-
|
|
445
|
-
escapedigits = options.delete(:escapedigits) || "#"
|
|
446
|
-
silence = options.delete(:silence) || 0
|
|
447
|
-
|
|
448
|
-
response_params = filename, format, escapedigits, maxduration, 0
|
|
449
|
-
response_values = []
|
|
450
|
-
|
|
451
|
-
if !options.has_key? :beep
|
|
452
|
-
response_params << 'BEEP'
|
|
453
|
-
elsif options[:beep]
|
|
454
|
-
play_soundfile options[:beep]
|
|
455
|
-
playback_response = get_variable('PLAYBACKSTATUS')
|
|
456
|
-
if playback_response != PLAYBACK_SUCCESS
|
|
457
|
-
response_values << :playback_error
|
|
458
|
-
response_values << playback_response
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
if silence > 0
|
|
463
|
-
response_params << "s=#{silence}"
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
resp = response 'RECORD FILE', *response_params
|
|
467
|
-
# If the user hangs up before the recording is entered, -1 is returned by asterisk and RECORDED_FILE
|
|
468
|
-
# will not contain the name of the file, even though it IS in fact recorded.
|
|
469
|
-
if resp.match /hangup/
|
|
470
|
-
response_values << :hangup
|
|
471
|
-
elsif resp.match /writefile/
|
|
472
|
-
response_values << :write_error
|
|
473
|
-
elsif resp.match /dtmf/
|
|
474
|
-
response_values << :success_dtmf
|
|
475
|
-
elsif resp.match /timeout/
|
|
476
|
-
response_values << :success_timeout
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
response_values
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
# Simulates pressing the specified digits over the current channel. Can be used to
|
|
483
|
-
# traverse a phone menu.
|
|
484
|
-
def dtmf(digits)
|
|
485
|
-
execute "SendDTMF", digits.to_s
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
# The with_next_message method...
|
|
489
|
-
def with_next_message(&block)
|
|
490
|
-
raise LocalJumpError, "Must supply a block" unless block_given?
|
|
491
|
-
block.call(next_message)
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
# This command should be used to advance to the next message in the Asterisk Comedian Voicemail application
|
|
495
|
-
def next_message
|
|
496
|
-
@call.inbox.pop
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
# This command should be used to check if a message is waiting on the Asterisk Comedian Voicemail application.
|
|
500
|
-
def messages_waiting?
|
|
501
|
-
not @call.inbox.empty?
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
# Creates an interactive menu for the caller.
|
|
505
|
-
#
|
|
506
|
-
# The following documentation was derived from a post on Jay Phillips' blog (see below).
|
|
507
|
-
#
|
|
508
|
-
# The menu() command solves the problem of building enormous input-fetching state machines in Ruby without first-class
|
|
509
|
-
# message passing facilities or an external DSL.
|
|
510
|
-
#
|
|
511
|
-
# Here is an example dialplan which uses the menu() command effectively.
|
|
512
|
-
#
|
|
513
|
-
# from_pstn {
|
|
514
|
-
# menu 'welcome', 'for-spanish-press-8', 'main-ivr',
|
|
515
|
-
# :timeout => 8.seconds, :tries => 3 do |link|
|
|
516
|
-
# link.shipment_status 1
|
|
517
|
-
# link.ordering 2
|
|
518
|
-
# link.representative 4
|
|
519
|
-
# link.spanish 8
|
|
520
|
-
# link.employee 900..999
|
|
521
|
-
#
|
|
522
|
-
# link.on_invalid { play 'invalid' }
|
|
523
|
-
#
|
|
524
|
-
# link.on_premature_timeout do |str|
|
|
525
|
-
# play 'sorry'
|
|
526
|
-
# end
|
|
527
|
-
#
|
|
528
|
-
# link.on_failure do
|
|
529
|
-
# play 'goodbye'
|
|
530
|
-
# hangup
|
|
531
|
-
# end
|
|
532
|
-
# end
|
|
533
|
-
# }
|
|
534
|
-
#
|
|
535
|
-
# shipment_status {
|
|
536
|
-
# # Fetch a tracking number and pass it to a web service.
|
|
537
|
-
# }
|
|
538
|
-
#
|
|
539
|
-
# ordering {
|
|
540
|
-
# # Enter another menu that lets them enter credit card
|
|
541
|
-
# # information and place their order over the phone.
|
|
542
|
-
# }
|
|
543
|
-
#
|
|
544
|
-
# representative {
|
|
545
|
-
# # Place the caller into a queue
|
|
546
|
-
# }
|
|
547
|
-
#
|
|
548
|
-
# spanish {
|
|
549
|
-
# # Special options for the spanish menu.
|
|
550
|
-
# }
|
|
551
|
-
#
|
|
552
|
-
# employee {
|
|
553
|
-
# dial "SIP/#{extension}" # Overly simplistic
|
|
554
|
-
# }
|
|
555
|
-
#
|
|
556
|
-
# The main detail to note is the declarations within the menu() command’s block. Each line seems to refer to a link object
|
|
557
|
-
# executing a seemingly arbitrary method with an argument that’s either a number or a Range of numbers. The +link+ object
|
|
558
|
-
# collects these arbitrary method invocations and assembles a set of rules. The seemingly arbitrary method name is the name
|
|
559
|
-
# of the context to which the menu should jump in case its argument (the pattern) is found to be a match.
|
|
560
|
-
#
|
|
561
|
-
# With these context names and patterns defined, the +menu()+ command plays in sequence the sound files you supply as
|
|
562
|
-
# arguments, stopping playback abruptly if the user enters a digit. If no digits were pressed when the files finish playing,
|
|
563
|
-
# it waits +:timeout+ seconds. If no digits are pressed after the timeout, it executes the +on_premature_timeout+ hook you
|
|
564
|
-
# define (if any) and then tries again a maximum of +:tries+ times. If digits are pressed that result in no possible match,
|
|
565
|
-
# it executes the +on_invalid+ hook. When/if all tries are exhausted with no positive match, it executes the +on_failure+
|
|
566
|
-
# hook after the other hook (e.g. +on_invalid+, then +on_failure+).
|
|
567
|
-
#
|
|
568
|
-
# When the +menu()+ state machine runs through the defined rules, it must distinguish between exact and potential matches.
|
|
569
|
-
# It's important to understand the differences between these and how they affect the overall outcome:
|
|
570
|
-
#
|
|
571
|
-
# |---------------|-------------------|------------------------------------------------------|
|
|
572
|
-
# | exact matches | potential matches | result |
|
|
573
|
-
# |---------------|-------------------|------------------------------------------------------|
|
|
574
|
-
# | 0 | 0 | Fail and start over |
|
|
575
|
-
# | 1 | 0 | Match found! |
|
|
576
|
-
# | 0 | >0 | Get another digit |
|
|
577
|
-
# | >1 | 0 | Go with the first exact match |
|
|
578
|
-
# | 1 | >0 | Get another digit. If timeout, use exact match |
|
|
579
|
-
# | >1 | >0 | Get another digit. If timeout, use first exact match |
|
|
580
|
-
# |---------------|-------------------|------------------------------------------------------|
|
|
581
|
-
#
|
|
582
|
-
# == Database integration
|
|
583
|
-
#
|
|
584
|
-
# To do database integration, I recommend programatically executing methods on the link object within the block. For example:
|
|
585
|
-
#
|
|
586
|
-
# menu do |link|
|
|
587
|
-
# for employee in Employee.find(:all)
|
|
588
|
-
# link.internal employee.extension
|
|
589
|
-
# end
|
|
590
|
-
# end
|
|
591
|
-
#
|
|
592
|
-
# or this more efficient and Rubyish way
|
|
593
|
-
#
|
|
594
|
-
# menu do |link|
|
|
595
|
-
# link.internal *Employee.find(:all).map(&:extension)
|
|
596
|
-
# end
|
|
597
|
-
#
|
|
598
|
-
# If this second example seems like too much Ruby magic, let me explain — +Employee.find(:all)+ effectively does a “SELECT *
|
|
599
|
-
# FROM employees” on the database with ActiveRecord, returning (what you’d think is) an Array. The +map(&:extension)+ is
|
|
600
|
-
# fanciness that means “replace every instance in this Array with the result of calling extension on that object”. Now we
|
|
601
|
-
# have an Array of every extension in the database. The splat operator (*) before the argument changes the argument from
|
|
602
|
-
# being one argument (an Array) into a sequence of n arguments, where n is the number of items in the Array it’s “splatting”.
|
|
603
|
-
# Lastly, these arguments are passed to the internal method, the name of a context which will handle dialing this user if one
|
|
604
|
-
# of the supplied patterns matches.
|
|
605
|
-
#
|
|
606
|
-
# == Handling a successful pattern match
|
|
607
|
-
#
|
|
608
|
-
# Which brings me to another important note. Let’s say that the user’s input successfully matched one of the patterns
|
|
609
|
-
# returned by that Employe.find... magic. When it jumps to the internal context, that context can access the variable entered
|
|
610
|
-
# through the extension variable. This was a tricky design decision that I think, overall, works great. It makes the +menu()+
|
|
611
|
-
# command feel much more first-class in the Adhearsion dialplan grammar and decouples the receiving context from the menu
|
|
612
|
-
# 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
|
|
613
|
-
# point, making menu() effectively a pipeline of re-creating the call.
|
|
614
|
-
#
|
|
615
|
-
# @see http://jicksta.com/articles/2008/02/11/menu-command Original Blog Post
|
|
616
|
-
def menu(*args, &block)
|
|
617
|
-
options = args.last.kind_of?(Hash) ? args.pop : {}
|
|
618
|
-
sound_files = args.flatten
|
|
619
|
-
|
|
620
|
-
menu_instance = Menu.new(options, &block)
|
|
621
|
-
|
|
622
|
-
initial_digit_prompt = sound_files.any?
|
|
623
|
-
|
|
624
|
-
# This method is basically one big begin/rescue block. When we start the Menu state machine by continue()ing, the state
|
|
625
|
-
# machine will pass messages back to this method in the form of Exceptions. This decoupling allows the menu system to
|
|
626
|
-
# work on, say, Freeswitch and Asterisk both.
|
|
627
|
-
begin
|
|
628
|
-
if menu_instance.should_continue?
|
|
629
|
-
menu_instance.continue
|
|
630
|
-
else
|
|
631
|
-
menu_instance.execute_failure_hook
|
|
632
|
-
return :failed
|
|
633
|
-
end
|
|
634
|
-
rescue Menu::MenuResult => result_of_menu
|
|
635
|
-
case result_of_menu
|
|
636
|
-
when Menu::MenuResultInvalid
|
|
637
|
-
menu_instance.execute_invalid_hook
|
|
638
|
-
menu_instance.restart!
|
|
639
|
-
when Menu::MenuGetAnotherDigit
|
|
640
|
-
|
|
641
|
-
next_digit = play_sound_files_for_menu(menu_instance, sound_files)
|
|
642
|
-
if next_digit
|
|
643
|
-
menu_instance << next_digit
|
|
644
|
-
else
|
|
645
|
-
# The user timed out entering another digit!
|
|
646
|
-
case result_of_menu
|
|
647
|
-
when Menu::MenuGetAnotherDigitOrFinish
|
|
648
|
-
# This raises a ControlPassingException
|
|
649
|
-
jump_to result_of_menu.match_payload, :extension => result_of_menu.new_extension
|
|
650
|
-
when Menu::MenuGetAnotherDigitOrTimeout
|
|
651
|
-
# This should execute premature_timeout AND reset if the number of retries
|
|
652
|
-
# has not been exhausted.
|
|
653
|
-
menu_instance.execute_timeout_hook
|
|
654
|
-
menu_instance.restart!
|
|
655
|
-
end
|
|
656
|
-
end
|
|
657
|
-
when Menu::MenuResultFound
|
|
658
|
-
jump_to result_of_menu.match_payload, :extension => result_of_menu.new_extension
|
|
659
|
-
else
|
|
660
|
-
raise "Unrecognized MenuResult! This may be a bug!"
|
|
661
|
-
end
|
|
662
|
-
|
|
663
|
-
# Retry will re-execute the begin block, preserving our changes to the menu_instance object.
|
|
664
|
-
retry
|
|
665
|
-
|
|
666
|
-
end
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
# Used to receive keypad input from the user. Digits are collected
|
|
670
|
-
# via DTMF (keypad) input until one of four things happens:
|
|
671
|
-
#
|
|
672
|
-
# 1. The number of digits you specify as the first argument is collected
|
|
673
|
-
# 2. The timeout elapses. You can specify the timeout by either:
|
|
674
|
-
# * Providing the :timeout option which applies for each awaited
|
|
675
|
-
# digit
|
|
676
|
-
# * Providing the :initial_timeout for 1st digit and
|
|
677
|
-
# :interdigit_timeout for subsequent digits.
|
|
678
|
-
# 3. The "#" key (or the key you specify with :accept_key) is pressed
|
|
679
|
-
# 4. You return true from a block you pass in (see "Currency Example" below)
|
|
680
|
-
#
|
|
681
|
-
# Usage examples
|
|
682
|
-
#
|
|
683
|
-
# input # Receives digits until the caller presses the "#" key
|
|
684
|
-
# input 3 # Receives three digits. Can be 0-9, * or #
|
|
685
|
-
# input 5, :accept_key => "*" # Receive at most 5 digits, stopping if '*' is pressed
|
|
686
|
-
# input 1, :timeout => 1.minute # Receive a single digit, returning an empty
|
|
687
|
-
# # string if the timeout is encountered
|
|
688
|
-
# input 3, :initial_timeout => 10.seconds, :interdigit_timeout => 5.seconds # Accept up to 3 digits,
|
|
689
|
-
# # waiting 10 seconds for the 1st digit
|
|
690
|
-
# # and 5 seconds for each subsequent digit.
|
|
691
|
-
# input 9, :timeout => 7, :accept_key => "0" # Receives nine digits, returning
|
|
692
|
-
# # when the timeout is encountered
|
|
693
|
-
# # or when the "0" key is pressed.
|
|
694
|
-
# input 3, :play => "you-sound-cute"
|
|
695
|
-
# input :play => ["if-this-is-correct-press", 1, "otherwise-press", 2]
|
|
696
|
-
# input :interruptible => false, :play => ["you-cannot-interrupt-this-message"] # Disallow DTMF (keypad) interruption
|
|
697
|
-
# # until after all files are played.
|
|
698
|
-
# input 3, :speak => {:text => 'How much wood could a woodchuck chuck?'} # Say an interruptible TTS phrase
|
|
699
|
-
# # before waiting for input
|
|
700
|
-
# input 5, :play => 'this-sound-file-might-not-exist', :speak => {:text => "Here's the TTS I say in case I can't find the sound file"}
|
|
701
|
-
#
|
|
702
|
-
# Currency Example
|
|
703
|
-
# # Use a block to describe what kind of data will satisfy our input. Your block will be invoked each time a new digit is
|
|
704
|
-
# # received. When your block returns true, it signals #input not to wait for digits anymore. In the example below, we are
|
|
705
|
-
# # expecting a "currency" value, like 10*99. If the data ends with 2 decimal places, we immediately stop waiting for input.
|
|
706
|
-
# input 7, :speak => {:text => "How much is that doggie in the window?"} { |value| value =~ /\*\d\d/ }
|
|
707
|
-
#
|
|
708
|
-
# When specifying files to play, the playback of the sequence of files will stop
|
|
709
|
-
# immediately when the user presses the first digit unless you set :interruptible
|
|
710
|
-
# to false.
|
|
711
|
-
#
|
|
712
|
-
# The :speak option takes a Hash. The Hash should include a :text key with a
|
|
713
|
-
# TTS phrase to say. All other keys accepted by {#speak} are also supported.
|
|
714
|
-
#
|
|
715
|
-
# The :timeout option works like a digit timeout, therefore each digit pressed
|
|
716
|
-
# causes the timer to reset. This is a much more user-friendly approach than an
|
|
717
|
-
# absolute timeout.
|
|
718
|
-
#
|
|
719
|
-
# Note that when the digit limit is not specified the :accept_key becomes "#".
|
|
720
|
-
# Otherwise there would be no way to end the collection of digits. You can
|
|
721
|
-
# obviously override this by passing in a new key with :accept_key.
|
|
722
|
-
#
|
|
723
|
-
# @return [String] The keypad input received. An empty string is returned in the
|
|
724
|
-
# absense of input. If the :accept_key argument was pressed, it
|
|
725
|
-
# will not appear in the output.
|
|
726
|
-
#
|
|
727
|
-
# @see http://mojolingo.com/blog/2011/getting_ready_for_adhearsion_1_2/ More information on :speak, and &block parameters
|
|
728
|
-
def input(*args, &block)
|
|
729
|
-
begin
|
|
730
|
-
input! *args, &block
|
|
731
|
-
rescue PlaybackError => e
|
|
732
|
-
ahn_log.agi.warn { e }
|
|
733
|
-
retry # If sound playback fails, play the remaining sound files and wait for digits
|
|
734
|
-
end
|
|
735
|
-
end
|
|
736
|
-
|
|
737
|
-
# Same as {#input}, but immediately raises an exception if sound playback fails
|
|
738
|
-
#
|
|
739
|
-
# @return (see #input)
|
|
740
|
-
# @raise [Adhearsion::VoIP::PlaybackError] If a sound file cannot be played
|
|
741
|
-
def input!(*args, &block)
|
|
742
|
-
options = args.last.kind_of?(Hash) ? args.pop : {}
|
|
743
|
-
number_of_digits = args.shift
|
|
744
|
-
|
|
745
|
-
options[:play] = [*options[:play]].compact
|
|
746
|
-
|
|
747
|
-
if options.has_key?(:interruptible) && options[:interruptible] == false
|
|
748
|
-
play_command = :play!
|
|
749
|
-
else
|
|
750
|
-
options[:interruptible] = true
|
|
751
|
-
play_command = :interruptible_play!
|
|
752
|
-
end
|
|
753
|
-
|
|
754
|
-
if options.has_key? :speak
|
|
755
|
-
raise ArgumentError unless options[:speak].is_a? Hash
|
|
756
|
-
raise ArgumentError, 'Must include a text string when requesting TTS fallback' unless options[:speak].has_key?(:text)
|
|
757
|
-
options[:speak][:interruptible] = options[:interruptible]
|
|
758
|
-
end
|
|
759
|
-
|
|
760
|
-
timeout = options[:timeout] || -1
|
|
761
|
-
initial_timeout = options[:initial_timeout] || timeout
|
|
762
|
-
interdigit_timeout = options[:interdigit_timeout] || timeout
|
|
763
|
-
terminating_key = options[:accept_key]
|
|
764
|
-
raise ArgumentError, ":accept_key must not be empty" if terminating_key == ""
|
|
765
|
-
terminating_key = if terminating_key
|
|
766
|
-
terminating_key.to_s
|
|
767
|
-
elsif number_of_digits.nil? && !terminating_key.equal?(false)
|
|
768
|
-
'#'
|
|
769
|
-
end
|
|
770
|
-
|
|
771
|
-
if number_of_digits && number_of_digits < 0
|
|
772
|
-
ahn_log.agi.warn "Giving -1 to #input is now deprecated. Do not specify a first " +
|
|
773
|
-
"argument to allow unlimited digits." if number_of_digits == -1
|
|
774
|
-
raise ArgumentError, "The number of digits must be positive!"
|
|
775
|
-
end
|
|
776
|
-
|
|
777
|
-
buffer = ''
|
|
778
|
-
if options[:play].any?
|
|
779
|
-
begin
|
|
780
|
-
# Consume the sound files one at a time. In the event of playback
|
|
781
|
-
# failure, this tells us which files remain unplayed.
|
|
782
|
-
while file = options[:play].shift
|
|
783
|
-
key = send play_command, file
|
|
784
|
-
key = nil if play_command == :play!
|
|
785
|
-
break if key
|
|
786
|
-
end
|
|
787
|
-
rescue PlaybackError
|
|
788
|
-
raise unless options[:speak]
|
|
789
|
-
key = speak options[:speak].delete(:text), options[:speak]
|
|
790
|
-
end
|
|
791
|
-
key ||= ''
|
|
792
|
-
elsif options[:speak]
|
|
793
|
-
key = speak(options[:speak].delete(:text), options[:speak]) || ''
|
|
794
|
-
else
|
|
795
|
-
key = ''
|
|
796
|
-
end
|
|
797
|
-
initial_timeout = nil if key.present?
|
|
798
|
-
|
|
799
|
-
loop do
|
|
800
|
-
return buffer if key.nil?
|
|
801
|
-
if terminating_key
|
|
802
|
-
if key == terminating_key
|
|
803
|
-
return buffer
|
|
804
|
-
else
|
|
805
|
-
buffer << key
|
|
806
|
-
return buffer if number_of_digits && number_of_digits == buffer.length
|
|
807
|
-
end
|
|
808
|
-
else
|
|
809
|
-
buffer << key
|
|
810
|
-
return buffer if number_of_digits && number_of_digits == buffer.length
|
|
811
|
-
end
|
|
812
|
-
return buffer if block_given? && yield(buffer)
|
|
813
|
-
key = wait_for_digit(initial_timeout || interdigit_timeout)
|
|
814
|
-
initial_timeout = nil
|
|
815
|
-
end
|
|
816
|
-
end
|
|
817
|
-
|
|
818
|
-
# Jumps to a context. An alternative to DialplanContextProc#+@. When jumping to a context, it will *not* resume executing
|
|
819
|
-
# the former context when the jumped-to context has finished executing. Make sure you don't have any
|
|
820
|
-
# +ensure+ closures which you expect to execute when the call has finished, as they will run when
|
|
821
|
-
# this method is called.
|
|
822
|
-
#
|
|
823
|
-
# You can optionally override certain dialplan variables when jumping to the context. A popular use of
|
|
824
|
-
# this is to redefine +extension+ (which this method automatically boxes with a PhoneNumber object) so
|
|
825
|
-
# you can effectively "restart" a call (from the perspective of the jumped-to context). When you override
|
|
826
|
-
# variables here, you're effectively blowing away the old variables. If you need them for some reason,
|
|
827
|
-
# you should assign the important ones to an instance variable first before calling this method.
|
|
828
|
-
def jump_to(context, overrides={})
|
|
829
|
-
context = lookup_context_with_name(context) if context.kind_of?(Symbol) || (context.kind_of?(String) && context =~ /^[\w_]+$/)
|
|
830
|
-
|
|
831
|
-
# JRuby has a bug that prevents us from correctly determining the class name.
|
|
832
|
-
# See: http://jira.codehaus.org/browse/JRUBY-5026
|
|
833
|
-
if !(context.kind_of?(Adhearsion::DialPlan::DialplanContextProc) || context.kind_of?(Proc))
|
|
834
|
-
raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException
|
|
835
|
-
end
|
|
836
|
-
|
|
837
|
-
if overrides.any?
|
|
838
|
-
overrides = overrides.symbolize_keys
|
|
839
|
-
if overrides.has_key?(:extension) && !overrides[:extension].kind_of?(Adhearsion::VoIP::DSL::PhoneNumber)
|
|
840
|
-
overrides[:extension] = Adhearsion::VoIP::DSL::PhoneNumber.new overrides[:extension]
|
|
841
|
-
end
|
|
842
|
-
|
|
843
|
-
overrides.each_pair do |key, value|
|
|
844
|
-
meta_def(key) { value }
|
|
845
|
-
end
|
|
846
|
-
end
|
|
847
|
-
|
|
848
|
-
raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context)
|
|
849
|
-
end
|
|
850
|
-
|
|
851
|
-
# Place a call in a queue to be answered by a registered agent. You must then call join!()
|
|
852
|
-
#
|
|
853
|
-
# @param [String] queue_name the queue name to place the caller in
|
|
854
|
-
# @return [Adhearsion::VoIP::Asterisk::Commands::QueueProxy] a queue proxy object
|
|
855
|
-
#
|
|
856
|
-
# @see http://www.voip-info.org/wiki-Asterisk+cmd+Queue Full information on the Asterisk Queue
|
|
857
|
-
# @see Adhearsion::VoIP::Asterisk::Commands::QueueProxy#join! join!() for further details
|
|
858
|
-
def queue(queue_name)
|
|
859
|
-
queue_name = queue_name.to_s
|
|
860
|
-
|
|
861
|
-
@queue_proxy_hash_lock = Mutex.new unless defined? @queue_proxy_hash_lock
|
|
862
|
-
@queue_proxy_hash_lock.synchronize do
|
|
863
|
-
@queue_proxy_hash ||= {}
|
|
864
|
-
if @queue_proxy_hash.has_key? queue_name
|
|
865
|
-
return @queue_proxy_hash[queue_name]
|
|
866
|
-
else
|
|
867
|
-
proxy = @queue_proxy_hash[queue_name] = QueueProxy.new(queue_name, self)
|
|
868
|
-
return proxy
|
|
869
|
-
end
|
|
870
|
-
end
|
|
871
|
-
end
|
|
872
|
-
|
|
873
|
-
# Get the status of the last dial(). Possible dial statuses include :answer,
|
|
874
|
-
# :busy, :no_answer, :cancelled, :congested, and :channel_unavailable.
|
|
875
|
-
# If :cancel is returned, the caller hung up before the callee picked up.
|
|
876
|
-
# If :congestion is returned, the dialed extension probably doesn't exist.
|
|
877
|
-
# If :channel_unavailable, the callee phone may not be registered.
|
|
878
|
-
def last_dial_status
|
|
879
|
-
DIAL_STATUSES[get_dial_status]
|
|
880
|
-
end
|
|
881
|
-
|
|
882
|
-
# @return [Boolean] true if your last call to dial() finished with the ANSWER state,
|
|
883
|
-
# as reported by Asterisk. false otherwise
|
|
884
|
-
def last_dial_successful?
|
|
885
|
-
last_dial_status == :answered
|
|
886
|
-
end
|
|
887
|
-
|
|
888
|
-
# Opposite of last_dial_successful?()
|
|
889
|
-
def last_dial_unsuccessful?
|
|
890
|
-
not last_dial_successful?
|
|
891
|
-
end
|
|
892
|
-
|
|
893
|
-
##
|
|
894
|
-
# @param [#to_s] text to speak using the TTS engine
|
|
895
|
-
# @param [Hash] options
|
|
896
|
-
# @param options [Symbol] :engine the engine to use. Currently supported engines are :cepstral and :unimrcp
|
|
897
|
-
# @param options [String] :barge_in_digits digits to allow the TTS to be interrupted with
|
|
898
|
-
#
|
|
899
|
-
def speak(text, options = {})
|
|
900
|
-
engine = options.delete(:engine) || AHN_CONFIG.asterisk.speech_engine || :none
|
|
901
|
-
options[:interruptible] = false unless options.has_key?(:interruptible)
|
|
902
|
-
SpeechEngines.send(engine, self, text.to_s, options)
|
|
903
|
-
end
|
|
904
|
-
|
|
905
|
-
module SpeechEngines
|
|
906
|
-
class InvalidSpeechEngine < StandardError; end
|
|
907
|
-
|
|
908
|
-
class << self
|
|
909
|
-
def cepstral(call, text, options = {})
|
|
910
|
-
# We need to aggressively escape commas so app_swift does not
|
|
911
|
-
# think they are arguments.
|
|
912
|
-
text.gsub! /,/, '\\\\,'
|
|
913
|
-
command = ['Swift', text]
|
|
914
|
-
|
|
915
|
-
if options[:interrupt_digits]
|
|
916
|
-
ahn_log.agi.warn 'Cepstral does not support specifying interrupt digits'
|
|
917
|
-
options[:interruptible] = true
|
|
918
|
-
end
|
|
919
|
-
# Wait for 1ms after speaking and collect no more than 1 digit
|
|
920
|
-
command += [1, 1] if options[:interruptible]
|
|
921
|
-
call.execute *command
|
|
922
|
-
call.get_variable('SWIFT_DTMF')
|
|
923
|
-
end
|
|
924
|
-
|
|
925
|
-
def unimrcp(call, text, options = {})
|
|
926
|
-
# app_unimrcp strips quotes, which will already be stripped by the AGI parser.
|
|
927
|
-
# To work around this bug, we have to actually quote the arguments twice, once
|
|
928
|
-
# in this method and again inside #execute.
|
|
929
|
-
# Example from the logs:
|
|
930
|
-
# AGI Input: EXEC MRCPSynth "<speak xmlns=\"http://www.w3.org/2001/10/synthesis\" version=\"1.0\" xml:lang=\"en-US\"> <voice name=\"Paul\"> <prosody rate=\"1.0\">Howdy, stranger. How are you today?</prosody> </voice> </speak>"
|
|
931
|
-
# [Aug 3 13:39:02] VERBOSE[8495] logger.c: -- AGI Script Executing Application: (MRCPSynth) Options: (<speak xmlns="http://www.w3.org/2001/10/synthesis" version="1.0" xml:lang="en-US"> <voice name="Paul"> <prosody rate="1.0">Howdy, stranger. How are you today?</prosody> </voice> </speak>)
|
|
932
|
-
# [Aug 3 13:39:02] NOTICE[8495] app_unimrcp.c: Text to synthesize is: <speak xmlns=http://www.w3.org/2001/10/synthesis version=1.0 xml:lang=en-US> <voice name=Paul> <prosody rate=1.0>Howdy, stranger. How are you today?</prosody> </voice> </speak>
|
|
933
|
-
command = ['MRCPSynth', text.gsub(/["\\]/) { |m| "\\#{m}" }]
|
|
934
|
-
args = []
|
|
935
|
-
if options[:interrupt_digits]
|
|
936
|
-
args << "i=#{options[:interrupt_digits]}"
|
|
937
|
-
else
|
|
938
|
-
args << "i=any" if options[:interruptible]
|
|
939
|
-
end
|
|
940
|
-
command << args.join('&') unless args.empty?
|
|
941
|
-
value = call.inline_return_value(call.execute *command)
|
|
942
|
-
value.to_i.chr unless value.nil?
|
|
943
|
-
end
|
|
944
|
-
|
|
945
|
-
def tropo(call, text, options = {})
|
|
946
|
-
command = ['Ask', text]
|
|
947
|
-
args = {}
|
|
948
|
-
args[:terminator] = options[:interrupt_digits].split('').join(',') if options[:interrupt_digits]
|
|
949
|
-
args[:bargein] = options[:interruptible] if options.has_key?(:interruptible)
|
|
950
|
-
command << args.to_json unless args.empty?
|
|
951
|
-
value = JSON.parse call.raw_response(*command).sub(/^200 result=/, '')
|
|
952
|
-
value['interpretation']
|
|
953
|
-
end
|
|
954
|
-
|
|
955
|
-
def festival(text, call, options = {})
|
|
956
|
-
raise NotImplementedError
|
|
957
|
-
end
|
|
958
|
-
|
|
959
|
-
def none(text, call, options = {})
|
|
960
|
-
raise InvalidSpeechEngine, "No speech engine selected. You must specify one in your Adhearsion config file."
|
|
961
|
-
end
|
|
962
|
-
|
|
963
|
-
def method_missing(engine_name, text, options = {})
|
|
964
|
-
raise InvalidSpeechEngine, "Unsupported speech engine #{engine_name} for speaking '#{text}'"
|
|
965
|
-
end
|
|
966
|
-
end
|
|
967
|
-
end
|
|
968
|
-
|
|
969
|
-
# A high-level way of enabling features you create/uncomment from features.conf.
|
|
970
|
-
#
|
|
971
|
-
# Certain Symbol features you enable (as defined in DYNAMIC_FEATURE_EXTENSIONS) have optional
|
|
972
|
-
# arguments that you can also specify here. The usage examples show how to do this.
|
|
973
|
-
#
|
|
974
|
-
# Usage examples:
|
|
975
|
-
#
|
|
976
|
-
# enable_feature :attended_transfer # Enables "atxfer"
|
|
977
|
-
#
|
|
978
|
-
# enable_feature :attended_transfer, :context => "my_dial" # Enables "atxfer" and then
|
|
979
|
-
# # sets "TRANSFER_CONTEXT" to :context's value
|
|
980
|
-
#
|
|
981
|
-
# enable_feature :blind_transfer, :context => 'my_dial' # Enables 'blindxfer' and sets TRANSFER_CONTEXT
|
|
982
|
-
#
|
|
983
|
-
# enable_feature "foobar" # Enables "foobar"
|
|
984
|
-
#
|
|
985
|
-
# enable_feature("dup"); enable_feature("dup") # Enables "dup" only once.
|
|
986
|
-
def enable_feature(feature_name, optional_options=nil)
|
|
987
|
-
if DYNAMIC_FEATURE_EXTENSIONS.has_key? feature_name
|
|
988
|
-
instance_exec(optional_options, &DYNAMIC_FEATURE_EXTENSIONS[feature_name])
|
|
989
|
-
else
|
|
990
|
-
raise ArgumentError, "You cannot supply optional options when the feature name is " +
|
|
991
|
-
"not internally recognized!" if optional_options
|
|
992
|
-
extend_dynamic_features_with feature_name
|
|
993
|
-
end
|
|
994
|
-
end
|
|
995
|
-
|
|
996
|
-
# Disables a feature name specified in features.conf. If you're disabling it, it was probably
|
|
997
|
-
# set by enable_feature().
|
|
998
|
-
#
|
|
999
|
-
# @param [String] feature_name
|
|
1000
|
-
def disable_feature(feature_name)
|
|
1001
|
-
enabled_features_variable = variable 'DYNAMIC_FEATURES'
|
|
1002
|
-
enabled_features = enabled_features_variable.split('#')
|
|
1003
|
-
if enabled_features.include? feature_name
|
|
1004
|
-
enabled_features.delete feature_name
|
|
1005
|
-
variable 'DYNAMIC_FEATURES' => enabled_features.join('#')
|
|
1006
|
-
end
|
|
1007
|
-
end
|
|
1008
|
-
|
|
1009
|
-
# Used to join a particular conference with the MeetMe application. To use MeetMe, be sure you
|
|
1010
|
-
# have a proper timing device configured on your Asterisk box. MeetMe is Asterisk's built-in
|
|
1011
|
-
# conferencing program.
|
|
1012
|
-
#
|
|
1013
|
-
# @param [String] conference_id
|
|
1014
|
-
# @param [Hash] options
|
|
1015
|
-
#
|
|
1016
|
-
# @see http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe Asterisk Meetme Application Information
|
|
1017
|
-
def join(conference_id, options={})
|
|
1018
|
-
conference_id = conference_id.to_s.scan(/\w/).join
|
|
1019
|
-
command_flags = options[:options].to_s # This is a passthrough string straight to Asterisk
|
|
1020
|
-
pin = options[:pin]
|
|
1021
|
-
raise ArgumentError, "A conference PIN number must be numerical!" if pin && pin.to_s !~ /^\d+$/
|
|
1022
|
-
|
|
1023
|
-
# To disable dynamic conference creation set :use_static_conf => true
|
|
1024
|
-
use_static_conf = options.has_key?(:use_static_conf) ? options[:use_static_conf] : false
|
|
1025
|
-
|
|
1026
|
-
# The 'd' option of MeetMe creates conferences dynamically.
|
|
1027
|
-
command_flags += 'd' unless (command_flags.include?('d') or use_static_conf)
|
|
1028
|
-
|
|
1029
|
-
execute "MeetMe", conference_id, command_flags, options[:pin]
|
|
1030
|
-
end
|
|
1031
|
-
|
|
1032
|
-
# Issue this command to access a channel variable that exists in the asterisk dialplan (i.e. extensions.conf)
|
|
1033
|
-
# Use get_variable to pass information from other modules or high level configurations from the asterisk dialplan
|
|
1034
|
-
# to the adhearsion dialplan.
|
|
1035
|
-
#
|
|
1036
|
-
# @param [String] variable_name
|
|
1037
|
-
#
|
|
1038
|
-
# @see: http://www.voip-info.org/wiki/view/get+variable Asterisk Get Variable
|
|
1039
|
-
def get_variable(variable_name)
|
|
1040
|
-
inline_result_with_return_value response "GET VARIABLE", variable_name
|
|
1041
|
-
end
|
|
1042
|
-
|
|
1043
|
-
# Pass information back to the asterisk dial plan.
|
|
1044
|
-
#
|
|
1045
|
-
# Keep in mind that the variables are not global variables. These variables only exist for the channel
|
|
1046
|
-
# related to the call that is being serviced by the particular instance of your adhearsion application.
|
|
1047
|
-
# You will not be able to pass information back to the asterisk dialplan for other instances of your adhearsion
|
|
1048
|
-
# application to share. Once the channel is "hungup" then the variables are cleared and their information is gone.
|
|
1049
|
-
#
|
|
1050
|
-
# @param [String] variable_name
|
|
1051
|
-
# @param [String] value
|
|
1052
|
-
#
|
|
1053
|
-
# @see http://www.voip-info.org/wiki/view/set+variable Asterisk Set Variable
|
|
1054
|
-
def set_variable(variable_name, value)
|
|
1055
|
-
response("SET VARIABLE", variable_name, value) == AGI_SUCCESSFUL_RESPONSE
|
|
1056
|
-
end
|
|
1057
|
-
|
|
1058
|
-
# Issue the command to add a custom SIP header to the current call channel
|
|
1059
|
-
# example use: sip_add_header("x-ahn-test", "rubyrox")
|
|
1060
|
-
#
|
|
1061
|
-
# @param[String] the name of the SIP header
|
|
1062
|
-
# @param[String] the value of the SIP header
|
|
1063
|
-
#
|
|
1064
|
-
# @return [String] the Asterisk response
|
|
1065
|
-
#
|
|
1066
|
-
# @see http://www.voip-info.org/wiki/index.php?page=Asterisk+cmd+SIPAddHeader Asterisk SIPAddHeader
|
|
1067
|
-
def sip_add_header(header, value)
|
|
1068
|
-
execute("SIPAddHeader", "#{header}: #{value}") == AGI_SUCCESSFUL_RESPONSE
|
|
1069
|
-
end
|
|
1070
|
-
|
|
1071
|
-
# Issue the command to fetch a SIP header from the current call channel
|
|
1072
|
-
# example use: sip_get_header("x-ahn-test")
|
|
1073
|
-
#
|
|
1074
|
-
# @param[String] the name of the SIP header to get
|
|
1075
|
-
#
|
|
1076
|
-
# @return [String] the Asterisk response
|
|
1077
|
-
#
|
|
1078
|
-
# @see http://www.voip-info.org/wiki/index.php?page=Asterisk+cmd+SIPGetHeader Asterisk SIPGetHeader
|
|
1079
|
-
def sip_get_header(header)
|
|
1080
|
-
get_variable("SIP_HEADER(#{header})")
|
|
1081
|
-
end
|
|
1082
|
-
alias :sip_header :sip_get_header
|
|
1083
|
-
|
|
1084
|
-
# Allows you to either set or get a channel variable from Asterisk.
|
|
1085
|
-
# The method takes a hash key/value pair if you would like to set a variable
|
|
1086
|
-
# Or a single string with the variable to get from Asterisk
|
|
1087
|
-
def variable(*args)
|
|
1088
|
-
if args.last.kind_of? Hash
|
|
1089
|
-
assignments = args.pop
|
|
1090
|
-
raise ArgumentError, "Can't mix variable setting and fetching!" if args.any?
|
|
1091
|
-
assignments.each_pair do |key, value|
|
|
1092
|
-
set_variable(key, value)
|
|
1093
|
-
end
|
|
1094
|
-
else
|
|
1095
|
-
if args.size == 1
|
|
1096
|
-
get_variable args.first
|
|
1097
|
-
else
|
|
1098
|
-
args.map { |var| get_variable(var) }
|
|
1099
|
-
end
|
|
1100
|
-
end
|
|
1101
|
-
end
|
|
1102
|
-
|
|
1103
|
-
# Send a caller to a voicemail box to leave a message.
|
|
1104
|
-
#
|
|
1105
|
-
# The method takes the mailbox_number of the user to leave a message for and a
|
|
1106
|
-
# greeting_option that will determine which message gets played to the caller.
|
|
1107
|
-
#
|
|
1108
|
-
# @see http://www.voip-info.org/tiki-index.php?page=Asterisk+cmd+VoiceMail Asterisk Voicemail
|
|
1109
|
-
def voicemail(*args)
|
|
1110
|
-
options_hash = args.last.kind_of?(Hash) ? args.pop : {}
|
|
1111
|
-
mailbox_number = args.shift
|
|
1112
|
-
greeting_option = options_hash.delete(:greeting)
|
|
1113
|
-
skip_option = options_hash.delete(:skip)
|
|
1114
|
-
raise ArgumentError, 'You supplied too many arguments!' if mailbox_number && options_hash.any?
|
|
1115
|
-
greeting_option = case greeting_option
|
|
1116
|
-
when :busy then 'b'
|
|
1117
|
-
when :unavailable then 'u'
|
|
1118
|
-
when nil then nil
|
|
1119
|
-
else raise ArgumentError, "Unrecognized greeting #{greeting_option}"
|
|
1120
|
-
end
|
|
1121
|
-
skip_option &&= 's'
|
|
1122
|
-
options = "#{greeting_option}#{skip_option}"
|
|
1123
|
-
|
|
1124
|
-
raise ArgumentError, "Mailbox cannot be blank!" if !mailbox_number.nil? && mailbox_number.blank?
|
|
1125
|
-
number_with_context = if mailbox_number then mailbox_number else
|
|
1126
|
-
raise ArgumentError, "You must supply ONE context name!" if options_hash.size != 1
|
|
1127
|
-
context_name, mailboxes = options_hash.to_a.first
|
|
1128
|
-
Array(mailboxes).map do |mailbox|
|
|
1129
|
-
raise ArgumentError, "Mailbox numbers must be numerical!" unless mailbox.to_s =~ /^\d+$/
|
|
1130
|
-
"#{mailbox}@#{context_name}"
|
|
1131
|
-
end.join('&')
|
|
1132
|
-
end
|
|
1133
|
-
execute('voicemail', number_with_context, options)
|
|
1134
|
-
case variable('VMSTATUS')
|
|
1135
|
-
when 'SUCCESS' then true
|
|
1136
|
-
when 'USEREXIT' then false
|
|
1137
|
-
else nil
|
|
1138
|
-
end
|
|
1139
|
-
end
|
|
1140
|
-
|
|
1141
|
-
# The voicemail_main method puts a caller into the voicemail system to fetch their voicemail
|
|
1142
|
-
# or set options for their voicemail box.
|
|
1143
|
-
#
|
|
1144
|
-
# @param [Hash] options
|
|
1145
|
-
#
|
|
1146
|
-
# @see http://www.voip-info.org/wiki-Asterisk+cmd+VoiceMailMain Asterisk VoiceMailMain Command
|
|
1147
|
-
def voicemail_main(options={})
|
|
1148
|
-
mailbox, context, folder = options.values_at :mailbox, :context, :folder
|
|
1149
|
-
authenticate = options.has_key?(:authenticate) ? options[:authenticate] : true
|
|
1150
|
-
|
|
1151
|
-
folder = if folder
|
|
1152
|
-
if folder.to_s =~ /^[\w_]+$/
|
|
1153
|
-
"a(#{folder})"
|
|
1154
|
-
else
|
|
1155
|
-
raise ArgumentError, "Voicemail folder must be alphanumerical/underscore characters only!"
|
|
1156
|
-
end
|
|
1157
|
-
elsif folder == ''
|
|
1158
|
-
raise "Folder name cannot be an empty String!"
|
|
1159
|
-
else
|
|
1160
|
-
nil
|
|
1161
|
-
end
|
|
1162
|
-
|
|
1163
|
-
real_mailbox = ""
|
|
1164
|
-
real_mailbox << "#{mailbox}" unless mailbox.blank?
|
|
1165
|
-
real_mailbox << "@#{context}" unless context.blank?
|
|
1166
|
-
|
|
1167
|
-
real_options = ""
|
|
1168
|
-
real_options << "s" if !authenticate
|
|
1169
|
-
real_options << folder unless folder.blank?
|
|
1170
|
-
|
|
1171
|
-
command_args = [real_mailbox]
|
|
1172
|
-
command_args << real_options unless real_options.blank?
|
|
1173
|
-
command_args.clear if command_args == [""]
|
|
1174
|
-
|
|
1175
|
-
execute 'VoiceMailMain', *command_args
|
|
1176
|
-
end
|
|
1177
|
-
|
|
1178
|
-
def check_voicemail
|
|
1179
|
-
ahn_log.agi.warn "THE check_voicemail() DIALPLAN METHOD WILL SOON BE DEPRECATED! CHANGE THIS TO voicemail_main() INSTEAD"
|
|
1180
|
-
voicemail_main
|
|
1181
|
-
end
|
|
1182
|
-
|
|
1183
|
-
# Dial an extension or "phone number" in asterisk.
|
|
1184
|
-
# Maps to the Asterisk DIAL command in the asterisk dialplan.
|
|
1185
|
-
#
|
|
1186
|
-
# @param [String] number represents the extension or "number" that asterisk should dial.
|
|
1187
|
-
# Be careful to not just specify a number like 5001, 9095551001
|
|
1188
|
-
# You must specify a properly formatted string as Asterisk would expect to use in order to understand
|
|
1189
|
-
# whether the call should be dialed using SIP, IAX, or some other means.
|
|
1190
|
-
#
|
|
1191
|
-
# @param [Hash] options
|
|
1192
|
-
#
|
|
1193
|
-
# +:caller_id+ - the caller id number to be used when the call is placed. It is advised you properly adhere to the
|
|
1194
|
-
# policy of VoIP termination providers with respect to caller id values.
|
|
1195
|
-
#
|
|
1196
|
-
# +:name+ - this is the name which should be passed with the caller ID information
|
|
1197
|
-
# if :name=>"John Doe" and :caller_id => "444-333-1000" then the compelete CID and name would be "John Doe" <4443331000>
|
|
1198
|
-
# support for caller id information varies from country to country and from one VoIP termination provider to another.
|
|
1199
|
-
#
|
|
1200
|
-
# +:for+ - this option can be thought of best as a timeout. i.e. timeout after :for if no one answers the call
|
|
1201
|
-
# For example, dial("SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => callerid)
|
|
1202
|
-
# this call will timeout after 15 seconds if 1 of the 3 extensions being dialed do not pick prior to the 15 second time limit
|
|
1203
|
-
#
|
|
1204
|
-
# +:options+ - This is a string of options like "Tr" which are supported by the asterisk DIAL application.
|
|
1205
|
-
# for a complete list of these options and their usage please check the link below.
|
|
1206
|
-
#
|
|
1207
|
-
# +:confirm+ - ?
|
|
1208
|
-
#
|
|
1209
|
-
# @example Make a call to the PSTN using my SIP provider for VoIP termination
|
|
1210
|
-
# dial("SIP/19095551001@my.sip.voip.terminator.us")
|
|
1211
|
-
#
|
|
1212
|
-
# @example Make 3 Simulataneous calls to the SIP extensions separated by & symbols, try for 15 seconds and use the callerid
|
|
1213
|
-
# for this call specified by the variable my_callerid
|
|
1214
|
-
# dial "SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => my_callerid
|
|
1215
|
-
#
|
|
1216
|
-
# @example Make a call using the IAX provider to the PSTN
|
|
1217
|
-
# dial("IAX2/my.id@voipjet/19095551234", :name=>"John Doe", :caller_id=>"9095551234")
|
|
1218
|
-
#
|
|
1219
|
-
# @see http://www.voip-info.org/wiki-Asterisk+cmd+Dial Asterisk Dial Command
|
|
1220
|
-
def dial(number, options={})
|
|
1221
|
-
*recognized_options = :caller_id, :name, :for, :options, :confirm
|
|
1222
|
-
|
|
1223
|
-
unrecognized_options = options.keys - recognized_options
|
|
1224
|
-
raise ArgumentError, "Unknown dial options: #{unrecognized_options.to_sentence}" if unrecognized_options.any?
|
|
1225
|
-
set_caller_id_name options[:name]
|
|
1226
|
-
set_caller_id_number options[:caller_id]
|
|
1227
|
-
confirm_option = dial_macro_option_compiler options[:confirm]
|
|
1228
|
-
all_options = options[:options]
|
|
1229
|
-
all_options = all_options ? all_options + confirm_option : confirm_option
|
|
1230
|
-
execute "Dial", number, options[:for], all_options
|
|
1231
|
-
end
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
# This implementation of dial() uses the experimental call routing DSL.
|
|
1235
|
-
#
|
|
1236
|
-
# def dial(number, options={})
|
|
1237
|
-
# rules = callable_routes_for number
|
|
1238
|
-
# return :no_route if rules.empty?
|
|
1239
|
-
# call_attempt_status = nil
|
|
1240
|
-
# rules.each do |provider|
|
|
1241
|
-
#
|
|
1242
|
-
# response = execute "Dial",
|
|
1243
|
-
# provider.format_number_for_platform(number),
|
|
1244
|
-
# timeout_from_dial_options(options),
|
|
1245
|
-
# asterisk_options_from_dial_options(options)
|
|
1246
|
-
#
|
|
1247
|
-
# call_attempt_status = last_dial_status
|
|
1248
|
-
# break if call_attempt_status == :answered
|
|
1249
|
-
# end
|
|
1250
|
-
# call_attempt_status
|
|
1251
|
-
# end
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
# Speaks the digits given as an argument. For example, "123" is spoken as "one two three".
|
|
1255
|
-
#
|
|
1256
|
-
# @param [String] digits
|
|
1257
|
-
def say_digits(digits)
|
|
1258
|
-
execute "saydigits", validate_digits(digits)
|
|
1259
|
-
end
|
|
1260
|
-
|
|
1261
|
-
# Get the number of seconds the given block takes to execute. This
|
|
1262
|
-
# is particularly useful in dialplans for tracking billable time. Note that
|
|
1263
|
-
# if the call is hung up during the block, you will need to rescue the
|
|
1264
|
-
# exception if you have some mission-critical logic after it with which
|
|
1265
|
-
# you're recording this return-value.
|
|
1266
|
-
#
|
|
1267
|
-
# @return [Float] number of seconds taken for block to execute
|
|
1268
|
-
def duration_of
|
|
1269
|
-
start_time = Time.now
|
|
1270
|
-
yield
|
|
1271
|
-
Time.now - start_time
|
|
1272
|
-
end
|
|
1273
|
-
|
|
1274
|
-
#
|
|
1275
|
-
# Play a sequence of files, stopping the playback if a digit is pressed.
|
|
1276
|
-
#
|
|
1277
|
-
# @return [String, nil] digit pressed, or nil if none
|
|
1278
|
-
#
|
|
1279
|
-
def interruptible_play(*files)
|
|
1280
|
-
result = nil
|
|
1281
|
-
files.flatten.each do |file|
|
|
1282
|
-
begin
|
|
1283
|
-
result = interruptible_play!(file)
|
|
1284
|
-
rescue PlaybackError => e
|
|
1285
|
-
# Ignore this exception and play the next file
|
|
1286
|
-
ahn_log.agi.warn e.message
|
|
1287
|
-
ensure
|
|
1288
|
-
break if result
|
|
1289
|
-
end
|
|
1290
|
-
end
|
|
1291
|
-
result
|
|
1292
|
-
end
|
|
1293
|
-
|
|
1294
|
-
#
|
|
1295
|
-
# Same as {#interruptible_play}, but immediately raises an exception if a sound file cannot be played.
|
|
1296
|
-
#
|
|
1297
|
-
# @return (see #interruptible_play)
|
|
1298
|
-
# @raise [Adhearsion::VoIP::PlaybackError] If a sound file cannot be played
|
|
1299
|
-
def interruptible_play!(*files)
|
|
1300
|
-
startpos = 0
|
|
1301
|
-
files.flatten.each do |file|
|
|
1302
|
-
result = stream_file_result_from response("STREAM FILE", file, "1234567890*#")
|
|
1303
|
-
if result[:endpos].to_i <= startpos
|
|
1304
|
-
raise Adhearsion::VoIP::PlaybackError, "The sound file could not opened to stream. The parsed response was #{result.inspect}"
|
|
1305
|
-
end
|
|
1306
|
-
return result[:digit] if result.has_key? :digit
|
|
1307
|
-
end
|
|
1308
|
-
nil
|
|
1309
|
-
end
|
|
1310
|
-
|
|
1311
|
-
##
|
|
1312
|
-
# Executes the SayPhonetic command. This command will read the text passed in
|
|
1313
|
-
# out load using the NATO phonetic alphabet.
|
|
1314
|
-
#
|
|
1315
|
-
# @param [String] Passed in as the text to read aloud
|
|
1316
|
-
#
|
|
1317
|
-
# @see http://www.voip-info.org/wiki/view/Asterisk+cmd+SayPhonetic Asterisk SayPhonetic Command
|
|
1318
|
-
def say_phonetic(text)
|
|
1319
|
-
execute "sayphonetic", text
|
|
1320
|
-
end
|
|
1321
|
-
|
|
1322
|
-
##
|
|
1323
|
-
# Executes the SayAlpha command. This command will read the text passed in
|
|
1324
|
-
# out loud, character-by-character.
|
|
1325
|
-
#
|
|
1326
|
-
# @param [String] Passed in as the text to read aloud
|
|
1327
|
-
#
|
|
1328
|
-
# @example Say "one a two dot pound"
|
|
1329
|
-
# say_chars "1a2.#"
|
|
1330
|
-
#
|
|
1331
|
-
# @see http://www.voip-info.org/wiki/view/Asterisk+cmd+SayAlpha Asterisk SayPhonetic Command
|
|
1332
|
-
def say_chars(text)
|
|
1333
|
-
execute "sayalpha", text
|
|
1334
|
-
end
|
|
1335
|
-
|
|
1336
|
-
# Plays the given Date, Time, or Integer (seconds since epoch)
|
|
1337
|
-
# using the given timezone and format.
|
|
1338
|
-
#
|
|
1339
|
-
# @param [Date|Time|DateTime] Time to be said.
|
|
1340
|
-
# @param [Hash] Additional options to specify how exactly to say time specified.
|
|
1341
|
-
#
|
|
1342
|
-
# +:timezone+ - Sends a timezone to asterisk. See /usr/share/zoneinfo for a list. Defaults to the machine timezone.
|
|
1343
|
-
# +:format+ - This is the format the time is to be said in. Defaults to "ABdY 'digits/at' IMp"
|
|
1344
|
-
#
|
|
1345
|
-
# @see http://www.voip-info.org/wiki/view/Asterisk+cmd+SayUnixTime
|
|
1346
|
-
def play_time(*args)
|
|
1347
|
-
argument, options = args.flatten
|
|
1348
|
-
options ||= {}
|
|
1349
|
-
|
|
1350
|
-
return false unless options.is_a? Hash
|
|
1351
|
-
|
|
1352
|
-
timezone = options.delete(:timezone) || ''
|
|
1353
|
-
format = options.delete(:format) || ''
|
|
1354
|
-
epoch = case argument
|
|
1355
|
-
when Time || DateTime
|
|
1356
|
-
argument.to_i
|
|
1357
|
-
when Date
|
|
1358
|
-
format = 'BdY' unless format.present?
|
|
1359
|
-
argument.to_time.to_i
|
|
1360
|
-
end
|
|
1361
|
-
|
|
1362
|
-
return false if epoch.nil?
|
|
1363
|
-
|
|
1364
|
-
execute :sayunixtime, epoch, timezone, format
|
|
1365
|
-
end
|
|
1366
|
-
|
|
1367
|
-
protected
|
|
1368
|
-
|
|
1369
|
-
# wait_for_digits waits for the input of digits based on the number of milliseconds
|
|
1370
|
-
def wait_for_digit(timeout=-1)
|
|
1371
|
-
timeout *= 1_000 if timeout != -1
|
|
1372
|
-
result = result_digit_from response("WAIT FOR DIGIT", timeout.to_i)
|
|
1373
|
-
(result == 0.chr) ? nil : result
|
|
1374
|
-
end
|
|
1375
|
-
|
|
1376
|
-
##
|
|
1377
|
-
# Deprecated name of interruptible_play(). This is a misspelling!
|
|
1378
|
-
#
|
|
1379
|
-
def interruptable_play(*files)
|
|
1380
|
-
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!'
|
|
1381
|
-
interruptible_play(*files)
|
|
1382
|
-
end
|
|
1383
|
-
|
|
1384
|
-
# allows setting of the callerid number of the call
|
|
1385
|
-
def set_caller_id_number(caller_id_num)
|
|
1386
|
-
return unless caller_id_num
|
|
1387
|
-
raise ArgumentError, "Caller ID must be numeric" if caller_id_num.to_s !~ /^\+?\d+$/
|
|
1388
|
-
variable "CALLERID(num)" => caller_id_num
|
|
1389
|
-
end
|
|
1390
|
-
|
|
1391
|
-
# allows the setting of the callerid name of the call
|
|
1392
|
-
def set_caller_id_name(caller_id_name)
|
|
1393
|
-
return unless caller_id_name
|
|
1394
|
-
variable "CALLERID(name)" => caller_id_name
|
|
1395
|
-
end
|
|
1396
|
-
|
|
1397
|
-
def timeout_from_dial_options(options)
|
|
1398
|
-
options[:for] || options[:timeout]
|
|
1399
|
-
end
|
|
1400
|
-
|
|
1401
|
-
def asterisk_options_from_dial_options(options)
|
|
1402
|
-
# TODO: Will become much more sophisticated soon to handle callerid, etc
|
|
1403
|
-
options[:options]
|
|
1404
|
-
end
|
|
1405
|
-
|
|
1406
|
-
def dial_macro_option_compiler(confirm_argument_value)
|
|
1407
|
-
defaults = { :macro => 'ahn_dial_confirmer',
|
|
1408
|
-
:timeout => 20.seconds,
|
|
1409
|
-
:play => "beep",
|
|
1410
|
-
:key => '#' }
|
|
1411
|
-
|
|
1412
|
-
case confirm_argument_value
|
|
1413
|
-
when true
|
|
1414
|
-
DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument(defaults)
|
|
1415
|
-
when false, nil
|
|
1416
|
-
''
|
|
1417
|
-
when Proc
|
|
1418
|
-
raise NotImplementedError, "Coming in the future, you can do :confirm => my_context."
|
|
1419
|
-
|
|
1420
|
-
when Hash
|
|
1421
|
-
options = defaults.merge confirm_argument_value
|
|
1422
|
-
if((confirm_argument_value.keys - defaults.keys).any?)
|
|
1423
|
-
raise ArgumentError, "Known options: #{defaults.keys.to_sentence}"
|
|
1424
|
-
end
|
|
1425
|
-
raise ArgumentError, "Bad macro name!" unless options[:macro].to_s =~ /^[\w_]+$/
|
|
1426
|
-
options[:timeout] = case options[:timeout]
|
|
1427
|
-
when Fixnum, ActiveSupport::Duration
|
|
1428
|
-
options[:timeout]
|
|
1429
|
-
when String
|
|
1430
|
-
raise ArgumentError, "Timeout must be numerical!" unless options[:timeout] =~ /^\d+$/
|
|
1431
|
-
options[:timeout].to_i
|
|
1432
|
-
when :none
|
|
1433
|
-
0
|
|
1434
|
-
else
|
|
1435
|
-
raise ArgumentError, "Unrecognized :timeout! #{options[:timeout].inspect}"
|
|
1436
|
-
end
|
|
1437
|
-
raise ArgumentError, "Unrecognized DTMF key: #{options[:key]}" unless options[:key].to_s =~ /^[\d#*]$/
|
|
1438
|
-
options[:play] = Array(options[:play]).join('++')
|
|
1439
|
-
DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument options
|
|
1440
|
-
|
|
1441
|
-
else
|
|
1442
|
-
raise ArgumentError, "Unrecognized :confirm option: #{confirm_argument_value.inspect}!"
|
|
1443
|
-
end
|
|
1444
|
-
end
|
|
1445
|
-
|
|
1446
|
-
def result_digit_from(response_string)
|
|
1447
|
-
raise ArgumentError, "Can't coerce nil into AGI response! This could be a bug!" unless response_string
|
|
1448
|
-
digit = response_string[/^#{response_prefix}(-?\d+(\.\d+)?)/,1]
|
|
1449
|
-
digit.to_i.chr if digit && digit.to_s != "-1"
|
|
1450
|
-
end
|
|
1451
|
-
|
|
1452
|
-
def stream_file_result_from(response_string)
|
|
1453
|
-
raise ArgumentError, "Can't coerce nil into AGI response! This could be a bug!" unless response_string
|
|
1454
|
-
params = {}
|
|
1455
|
-
digit, endpos = response_string.match(/^#{response_prefix}(-?\d+) endpos=(\d+)/).values_at 1, 2
|
|
1456
|
-
params[:digit] = digit.to_i.chr unless digit == "0" || digit.to_s == "-1"
|
|
1457
|
-
params[:endpos] = endpos.to_i if endpos
|
|
1458
|
-
params
|
|
1459
|
-
end
|
|
1460
|
-
|
|
1461
|
-
def extract_input_from(result)
|
|
1462
|
-
return false if error?(result)
|
|
1463
|
-
# return false if input_timed_out?(result)
|
|
1464
|
-
|
|
1465
|
-
# This regexp doesn't match if there was a timeout with no
|
|
1466
|
-
# inputted digits, therefore returning nil.
|
|
1467
|
-
|
|
1468
|
-
result[/^#{response_prefix}([\d*]+)/, 1]
|
|
1469
|
-
end
|
|
1470
|
-
|
|
1471
|
-
def extract_variable_from(result)
|
|
1472
|
-
return false if error?(result)
|
|
1473
|
-
result[/^#{response_prefix}1 \((.+)\)/, 1]
|
|
1474
|
-
end
|
|
1475
|
-
|
|
1476
|
-
def get_dial_status
|
|
1477
|
-
dial_status = variable('DIALSTATUS')
|
|
1478
|
-
dial_status ? dial_status.downcase.to_sym : :cancelled
|
|
1479
|
-
end
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
def play_numeric(argument)
|
|
1483
|
-
if argument.kind_of?(Numeric) || argument =~ /^\d+$/
|
|
1484
|
-
execute(:saynumber, argument)
|
|
1485
|
-
end
|
|
1486
|
-
end
|
|
1487
|
-
|
|
1488
|
-
# Instruct Asterisk to play a sound file to the channel
|
|
1489
|
-
def play_soundfile(argument)
|
|
1490
|
-
execute(:playback, argument)
|
|
1491
|
-
get_variable('PLAYBACKSTATUS') == PLAYBACK_SUCCESS
|
|
1492
|
-
end
|
|
1493
|
-
alias :play_string :play_soundfile
|
|
1494
|
-
|
|
1495
|
-
# Like play_soundfile, but this will raise Exceptions if there's a problem.
|
|
1496
|
-
#
|
|
1497
|
-
# @return [true]
|
|
1498
|
-
# @raise [Adhearsion::VoIP::PlaybackError] If a sound file cannot be played
|
|
1499
|
-
# @see http://www.voip-info.org/wiki/view/Asterisk+cmd+Playback More information on the Asterisk Playback command
|
|
1500
|
-
def play_soundfile!(argument)
|
|
1501
|
-
response = execute :playback, argument
|
|
1502
|
-
playback = get_variable 'PLAYBACKSTATUS'
|
|
1503
|
-
return true if playback == PLAYBACK_SUCCESS
|
|
1504
|
-
raise PlaybackError, "Playback failed with PLAYBACKSTATUS: #{playback.inspect}. The raw response was #{response.inspect}."
|
|
1505
|
-
end
|
|
1506
|
-
alias :play_string! :play_soundfile!
|
|
1507
|
-
|
|
1508
|
-
def play_sound_files_for_menu(menu_instance, sound_files)
|
|
1509
|
-
digit = nil
|
|
1510
|
-
if sound_files.any? && menu_instance.digit_buffer_empty?
|
|
1511
|
-
digit = interruptible_play(*sound_files)
|
|
1512
|
-
end
|
|
1513
|
-
digit || wait_for_digit(menu_instance.timeout)
|
|
1514
|
-
end
|
|
1515
|
-
|
|
1516
|
-
def extend_dynamic_features_with(feature_name)
|
|
1517
|
-
current_variable = variable("DYNAMIC_FEATURES") || ''
|
|
1518
|
-
enabled_features = current_variable.split '#'
|
|
1519
|
-
unless enabled_features.include? feature_name
|
|
1520
|
-
enabled_features << feature_name
|
|
1521
|
-
variable "DYNAMIC_FEATURES" => enabled_features.join('#')
|
|
1522
|
-
end
|
|
1523
|
-
end
|
|
1524
|
-
|
|
1525
|
-
def jump_to_context_with_name(context_name)
|
|
1526
|
-
context_lambda = lookup_context_with_name context_name
|
|
1527
|
-
raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context_lambda)
|
|
1528
|
-
end
|
|
1529
|
-
|
|
1530
|
-
def lookup_context_with_name(context_name)
|
|
1531
|
-
begin
|
|
1532
|
-
send context_name
|
|
1533
|
-
rescue NameError
|
|
1534
|
-
raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException
|
|
1535
|
-
end
|
|
1536
|
-
end
|
|
1537
|
-
|
|
1538
|
-
def redefine_extension_to_be(new_extension)
|
|
1539
|
-
new_extension = Adhearsion::VoIP::DSL::PhoneNumber.new new_extension
|
|
1540
|
-
meta_def(:extension) { new_extension }
|
|
1541
|
-
end
|
|
1542
|
-
|
|
1543
|
-
def to_pbx
|
|
1544
|
-
io
|
|
1545
|
-
end
|
|
1546
|
-
|
|
1547
|
-
def from_pbx
|
|
1548
|
-
io
|
|
1549
|
-
end
|
|
1550
|
-
|
|
1551
|
-
def validate_digits(digits)
|
|
1552
|
-
digits.to_s.tap do |digits_as_string|
|
|
1553
|
-
raise ArgumentError, "Can only be called with valid digits!" unless digits_as_string =~ /^[0-9*#-]+$/
|
|
1554
|
-
end
|
|
1555
|
-
end
|
|
1556
|
-
|
|
1557
|
-
def error?(result)
|
|
1558
|
-
result.to_s[/^#{response_prefix}(?:-\d+)/]
|
|
1559
|
-
end
|
|
1560
|
-
|
|
1561
|
-
# timeout with pressed digits: 200 result=<digits> (timeout)
|
|
1562
|
-
# timeout without pressed digits: 200 result= (timeout)
|
|
1563
|
-
# @see http://www.voip-info.org/wiki/view/get+data AGI Get Data
|
|
1564
|
-
def input_timed_out?(result)
|
|
1565
|
-
result.starts_with?(response_prefix) && result.ends_with?('(timeout)')
|
|
1566
|
-
end
|
|
1567
|
-
|
|
1568
|
-
def io
|
|
1569
|
-
call.io
|
|
1570
|
-
end
|
|
1571
|
-
|
|
1572
|
-
def response_prefix
|
|
1573
|
-
RESPONSE_PREFIX
|
|
1574
|
-
end
|
|
1575
|
-
|
|
1576
|
-
class QueueProxy
|
|
1577
|
-
|
|
1578
|
-
class << self
|
|
1579
|
-
|
|
1580
|
-
def format_join_hash_key_arguments(options)
|
|
1581
|
-
|
|
1582
|
-
bad_argument = lambda do |(key, value)|
|
|
1583
|
-
raise ArgumentError, "Unrecognize value for #{key.inspect} -- #{value.inspect}"
|
|
1584
|
-
end
|
|
1585
|
-
|
|
1586
|
-
# Direct Queue() arguments:
|
|
1587
|
-
timeout = options.delete :timeout
|
|
1588
|
-
announcement = options.delete :announce
|
|
1589
|
-
|
|
1590
|
-
# Terse single-character options
|
|
1591
|
-
ring_style = options.delete :play
|
|
1592
|
-
allow_hangup = options.delete :allow_hangup
|
|
1593
|
-
allow_transfer = options.delete :allow_transfer
|
|
1594
|
-
agi = options.delete :agi
|
|
1595
|
-
|
|
1596
|
-
raise ArgumentError, "Unrecognized args to join!: #{options.inspect}" if options.any?
|
|
1597
|
-
|
|
1598
|
-
ring_style = case ring_style
|
|
1599
|
-
when :ringing then 'r'
|
|
1600
|
-
when :music then ''
|
|
1601
|
-
when nil
|
|
1602
|
-
else bad_argument[:play => ring_style]
|
|
1603
|
-
end.to_s
|
|
1604
|
-
|
|
1605
|
-
allow_hangup = case allow_hangup
|
|
1606
|
-
when :caller then 'H'
|
|
1607
|
-
when :agent then 'h'
|
|
1608
|
-
when :everyone then 'Hh'
|
|
1609
|
-
when nil
|
|
1610
|
-
else bad_argument[:allow_hangup => allow_hangup]
|
|
1611
|
-
end.to_s
|
|
1612
|
-
|
|
1613
|
-
allow_transfer = case allow_transfer
|
|
1614
|
-
when :caller then 'T'
|
|
1615
|
-
when :agent then 't'
|
|
1616
|
-
when :everyone then 'Tt'
|
|
1617
|
-
when nil
|
|
1618
|
-
else bad_argument[:allow_transfer => allow_transfer]
|
|
1619
|
-
end.to_s
|
|
1620
|
-
|
|
1621
|
-
terse_character_options = ring_style + allow_transfer + allow_hangup
|
|
1622
|
-
|
|
1623
|
-
[terse_character_options, '', announcement, timeout, agi].map(&:to_s)
|
|
1624
|
-
end
|
|
1625
|
-
|
|
1626
|
-
end
|
|
1627
|
-
|
|
1628
|
-
attr_reader :name, :environment
|
|
1629
|
-
def initialize(name, environment)
|
|
1630
|
-
@name, @environment = name, environment
|
|
1631
|
-
end
|
|
1632
|
-
|
|
1633
|
-
# Makes the current channel join the queue.
|
|
1634
|
-
#
|
|
1635
|
-
# @param [Hash] options
|
|
1636
|
-
#
|
|
1637
|
-
# :timeout - The number of seconds to wait for an agent to answer
|
|
1638
|
-
# :play - Can be :ringing or :music.
|
|
1639
|
-
# :announce - A sound file to play instead of the normal queue announcement.
|
|
1640
|
-
# :allow_transfer - Can be :caller, :agent, or :everyone. Allow someone to transfer the call.
|
|
1641
|
-
# :allow_hangup - Can be :caller, :agent, or :everyone. Allow someone to hangup with the * key.
|
|
1642
|
-
# :agi - An AGI script to be called on the calling parties channel just before being connected.
|
|
1643
|
-
#
|
|
1644
|
-
# @example
|
|
1645
|
-
# queue('sales').join!
|
|
1646
|
-
# @example
|
|
1647
|
-
# queue('sales').join! :timeout => 1.minute
|
|
1648
|
-
# @example
|
|
1649
|
-
# queue('sales').join! :play => :music
|
|
1650
|
-
# @example
|
|
1651
|
-
# queue('sales').join! :play => :ringing
|
|
1652
|
-
# @example
|
|
1653
|
-
# queue('sales').join! :announce => "custom/special-queue-announcement"
|
|
1654
|
-
# @example
|
|
1655
|
-
# queue('sales').join! :allow_transfer => :caller
|
|
1656
|
-
# @example
|
|
1657
|
-
# queue('sales').join! :allow_transfer => :agent
|
|
1658
|
-
# @example
|
|
1659
|
-
# queue('sales').join! :allow_hangup => :caller
|
|
1660
|
-
# @example
|
|
1661
|
-
# queue('sales').join! :allow_hangup => :agent
|
|
1662
|
-
# @example
|
|
1663
|
-
# queue('sales').join! :allow_hangup => :everyone
|
|
1664
|
-
# @example
|
|
1665
|
-
# queue('sales').join! :agi => 'agi://localhost/sales_queue_callback'
|
|
1666
|
-
# @example
|
|
1667
|
-
# queue('sales').join! :allow_transfer => :agent, :timeout => 30.seconds,
|
|
1668
|
-
def join!(options={})
|
|
1669
|
-
environment.execute("queue", name, *self.class.format_join_hash_key_arguments(options))
|
|
1670
|
-
normalize_queue_status_variable environment.variable("QUEUESTATUS")
|
|
1671
|
-
end
|
|
1672
|
-
|
|
1673
|
-
# Get the agents associated with a queue
|
|
1674
|
-
#
|
|
1675
|
-
# @param [Hash] options
|
|
1676
|
-
# @return [QueueAgentsListProxy]
|
|
1677
|
-
def agents(options={})
|
|
1678
|
-
cached = options.has_key?(:cache) ? options.delete(:cache) : true
|
|
1679
|
-
raise ArgumentError, "Unrecognized arguments to agents(): #{options.inspect}" if options.keys.any?
|
|
1680
|
-
if cached
|
|
1681
|
-
@cached_proxy ||= QueueAgentsListProxy.new(self, true)
|
|
1682
|
-
else
|
|
1683
|
-
@uncached_proxy ||= QueueAgentsListProxy.new(self, false)
|
|
1684
|
-
end
|
|
1685
|
-
end
|
|
1686
|
-
|
|
1687
|
-
# Check how many channels are waiting in the queue
|
|
1688
|
-
# @return [Integer]
|
|
1689
|
-
# @raise QueueDoesNotExistError
|
|
1690
|
-
def waiting_count
|
|
1691
|
-
raise QueueDoesNotExistError.new(name) unless exists?
|
|
1692
|
-
environment.variable("QUEUE_WAITING_COUNT(#{name})").to_i
|
|
1693
|
-
end
|
|
1694
|
-
|
|
1695
|
-
# Check whether the waiting count is zero
|
|
1696
|
-
# @return [Boolean]
|
|
1697
|
-
def empty?
|
|
1698
|
-
waiting_count == 0
|
|
1699
|
-
end
|
|
1700
|
-
|
|
1701
|
-
# Check whether any calls are waiting in the queue
|
|
1702
|
-
# @return [Boolean]
|
|
1703
|
-
def any?
|
|
1704
|
-
waiting_count > 0
|
|
1705
|
-
end
|
|
1706
|
-
|
|
1707
|
-
# Check whether a queue exists/is defined in Asterisk
|
|
1708
|
-
# @return [Boolean]
|
|
1709
|
-
def exists?
|
|
1710
|
-
environment.execute('RemoveQueueMember', name, 'SIP/AdhearsionQueueExistenceCheck')
|
|
1711
|
-
environment.variable("RQMSTATUS") != 'NOSUCHQUEUE'
|
|
1712
|
-
end
|
|
1713
|
-
|
|
1714
|
-
private
|
|
1715
|
-
|
|
1716
|
-
# Ensure the queue exists by interpreting the QUEUESTATUS variable
|
|
1717
|
-
#
|
|
1718
|
-
# According to http://www.voip-info.org/wiki/view/Asterisk+cmd+Queue
|
|
1719
|
-
# possible values are:
|
|
1720
|
-
#
|
|
1721
|
-
# TIMEOUT => :timeout
|
|
1722
|
-
# FULL => :full
|
|
1723
|
-
# JOINEMPTY => :joinempty
|
|
1724
|
-
# LEAVEEMPTY => :leaveempty
|
|
1725
|
-
# JOINUNAVAIL => :joinunavail
|
|
1726
|
-
# LEAVEUNAVAIL => :leaveunavail
|
|
1727
|
-
# CONTINUE => :continue
|
|
1728
|
-
#
|
|
1729
|
-
# If the QUEUESTATUS variable is not set the call was successfully connected,
|
|
1730
|
-
# and Adhearsion will return :completed.
|
|
1731
|
-
#
|
|
1732
|
-
# @param [String] QUEUESTATUS variable from Asterisk
|
|
1733
|
-
# @return [Symbol] Symbolized version of QUEUESTATUS
|
|
1734
|
-
# @raise QueueDoesNotExistError
|
|
1735
|
-
def normalize_queue_status_variable(variable)
|
|
1736
|
-
variable = "COMPLETED" if variable.nil?
|
|
1737
|
-
variable.downcase.to_sym
|
|
1738
|
-
end
|
|
1739
|
-
|
|
1740
|
-
class QueueAgentsListProxy
|
|
1741
|
-
|
|
1742
|
-
include Enumerable
|
|
1743
|
-
|
|
1744
|
-
attr_reader :proxy, :agents
|
|
1745
|
-
def initialize(proxy, cached=false)
|
|
1746
|
-
@proxy = proxy
|
|
1747
|
-
@cached = cached
|
|
1748
|
-
end
|
|
1749
|
-
|
|
1750
|
-
def count
|
|
1751
|
-
if cached? && @cached_count
|
|
1752
|
-
@cached_count
|
|
1753
|
-
else
|
|
1754
|
-
@cached_count = proxy.environment.variable("QUEUE_MEMBER_COUNT(#{proxy.name})").to_i
|
|
1755
|
-
end
|
|
1756
|
-
end
|
|
1757
|
-
alias size count
|
|
1758
|
-
alias length count
|
|
1759
|
-
|
|
1760
|
-
# @param [Hash] args
|
|
1761
|
-
# :name value will be viewable in the queue_log
|
|
1762
|
-
# :penalty is the penalty assigned to this agent for answering calls on this queue
|
|
1763
|
-
def new(*args)
|
|
1764
|
-
|
|
1765
|
-
options = args.last.kind_of?(Hash) ? args.pop : {}
|
|
1766
|
-
interface = args.shift
|
|
1767
|
-
|
|
1768
|
-
raise ArgumentError, "You must specify an interface to add." if interface.nil?
|
|
1769
|
-
raise ArgumentError, "You may only supply an interface and a Hash argument!" if args.any?
|
|
1770
|
-
|
|
1771
|
-
penalty = options.delete(:penalty) || ''
|
|
1772
|
-
name = options.delete(:name) || ''
|
|
1773
|
-
state_interface = options.delete(:state_interface) || ''
|
|
1774
|
-
|
|
1775
|
-
raise ArgumentError, "Unrecognized argument(s): #{options.inspect}" if options.any?
|
|
1776
|
-
|
|
1777
|
-
proxy.environment.execute("AddQueueMember", proxy.name, interface, penalty, '', name, state_interface)
|
|
1778
|
-
|
|
1779
|
-
added = case proxy.environment.variable("AQMSTATUS")
|
|
1780
|
-
when "ADDED" then true
|
|
1781
|
-
when "MEMBERALREADY" then false
|
|
1782
|
-
when "NOSUCHQUEUE" then raise QueueDoesNotExistError.new(proxy.name)
|
|
1783
|
-
else
|
|
1784
|
-
raise "UNRECOGNIZED AQMSTATUS VALUE!"
|
|
1785
|
-
end
|
|
1786
|
-
|
|
1787
|
-
if added
|
|
1788
|
-
check_agent_cache!
|
|
1789
|
-
AgentProxy.new(interface, proxy).tap do |agent_proxy|
|
|
1790
|
-
@agents << agent_proxy
|
|
1791
|
-
end
|
|
1792
|
-
else
|
|
1793
|
-
false
|
|
1794
|
-
end
|
|
1795
|
-
end
|
|
1796
|
-
|
|
1797
|
-
# Logs a pre-defined agent into this queue and waits for calls. Pass in :silent => true to stop
|
|
1798
|
-
# the message which says "Agent logged in".
|
|
1799
|
-
def login!(*args)
|
|
1800
|
-
options = args.last.kind_of?(Hash) ? args.pop : {}
|
|
1801
|
-
|
|
1802
|
-
silent = options.delete(:silent).equal?(false) ? '' : 's'
|
|
1803
|
-
id = args.shift
|
|
1804
|
-
id &&= AgentProxy.id_from_agent_channel(id)
|
|
1805
|
-
raise ArgumentError, "Unrecognized Hash options to login(): #{options.inspect}" if options.any?
|
|
1806
|
-
raise ArgumentError, "Unrecognized argument to login(): #{args.inspect}" if args.any?
|
|
1807
|
-
|
|
1808
|
-
proxy.environment.execute('AgentLogin', id, silent)
|
|
1809
|
-
end
|
|
1810
|
-
|
|
1811
|
-
# Removes the current channel from this queue
|
|
1812
|
-
def logout!
|
|
1813
|
-
# TODO: DRY this up. Repeated in the AgentProxy...
|
|
1814
|
-
proxy.environment.execute 'RemoveQueueMember', proxy.name
|
|
1815
|
-
case proxy.environment.variable("RQMSTATUS")
|
|
1816
|
-
when "REMOVED" then true
|
|
1817
|
-
when "NOTINQUEUE" then false
|
|
1818
|
-
when "NOSUCHQUEUE"
|
|
1819
|
-
raise QueueDoesNotExistError.new(proxy.name)
|
|
1820
|
-
else
|
|
1821
|
-
raise "Unrecognized RQMSTATUS variable!"
|
|
1822
|
-
end
|
|
1823
|
-
end
|
|
1824
|
-
|
|
1825
|
-
def each(&block)
|
|
1826
|
-
check_agent_cache!
|
|
1827
|
-
agents.each(&block)
|
|
1828
|
-
end
|
|
1829
|
-
|
|
1830
|
-
def first
|
|
1831
|
-
check_agent_cache!
|
|
1832
|
-
agents.first
|
|
1833
|
-
end
|
|
1834
|
-
|
|
1835
|
-
def last
|
|
1836
|
-
check_agent_cache!
|
|
1837
|
-
agents.last
|
|
1838
|
-
end
|
|
1839
|
-
|
|
1840
|
-
def cached?
|
|
1841
|
-
@cached
|
|
1842
|
-
end
|
|
1843
|
-
|
|
1844
|
-
def to_a
|
|
1845
|
-
check_agent_cache!
|
|
1846
|
-
@agents
|
|
1847
|
-
end
|
|
1848
|
-
|
|
1849
|
-
private
|
|
1850
|
-
|
|
1851
|
-
def check_agent_cache!
|
|
1852
|
-
if cached?
|
|
1853
|
-
load_agents! unless agents
|
|
1854
|
-
else
|
|
1855
|
-
load_agents!
|
|
1856
|
-
end
|
|
1857
|
-
end
|
|
1858
|
-
|
|
1859
|
-
def load_agents!
|
|
1860
|
-
raw_data = proxy.environment.variable "QUEUE_MEMBER_LIST(#{proxy.name})"
|
|
1861
|
-
@agents = raw_data.split(',').map(&:strip).reject(&:empty?).map do |agent|
|
|
1862
|
-
AgentProxy.new(agent, proxy)
|
|
1863
|
-
end
|
|
1864
|
-
@cached_count = @agents.size
|
|
1865
|
-
end
|
|
1866
|
-
|
|
1867
|
-
end
|
|
1868
|
-
|
|
1869
|
-
class AgentProxy
|
|
1870
|
-
|
|
1871
|
-
SUPPORTED_METADATA_NAMES = %w[status password name mohclass exten channel] unless defined? SUPPORTED_METADATA_NAMES
|
|
1872
|
-
|
|
1873
|
-
class << self
|
|
1874
|
-
def id_from_agent_channel(id)
|
|
1875
|
-
id = id.to_s
|
|
1876
|
-
id.starts_with?('Agent/') ? id[%r[^Agent/(.+)$],1] : id
|
|
1877
|
-
end
|
|
1878
|
-
end
|
|
1879
|
-
|
|
1880
|
-
attr_reader :interface, :proxy, :queue_name, :id
|
|
1881
|
-
def initialize(interface, proxy)
|
|
1882
|
-
@interface = interface
|
|
1883
|
-
@id = self.class.id_from_agent_channel interface
|
|
1884
|
-
@proxy = proxy
|
|
1885
|
-
@queue_name = proxy.name
|
|
1886
|
-
end
|
|
1887
|
-
|
|
1888
|
-
def remove!
|
|
1889
|
-
proxy.environment.execute 'RemoveQueueMember', queue_name, interface
|
|
1890
|
-
case proxy.environment.variable("RQMSTATUS")
|
|
1891
|
-
when "REMOVED" then true
|
|
1892
|
-
when "NOTINQUEUE" then false
|
|
1893
|
-
when "NOSUCHQUEUE"
|
|
1894
|
-
raise QueueDoesNotExistError.new(queue_name)
|
|
1895
|
-
else
|
|
1896
|
-
raise "Unrecognized RQMSTATUS variable!"
|
|
1897
|
-
end
|
|
1898
|
-
end
|
|
1899
|
-
|
|
1900
|
-
# Pauses the given agent for this queue only. If you wish to pause this agent
|
|
1901
|
-
# for all queues, pass in :everywhere => true. Returns true if the agent was
|
|
1902
|
-
# successfully paused and false if the agent was not found.
|
|
1903
|
-
def pause!(options={})
|
|
1904
|
-
everywhere = options.delete(:everywhere)
|
|
1905
|
-
args = [(everywhere ? nil : queue_name), interface]
|
|
1906
|
-
proxy.environment.execute('PauseQueueMember', *args)
|
|
1907
|
-
case proxy.environment.variable("PQMSTATUS")
|
|
1908
|
-
when "PAUSED" then true
|
|
1909
|
-
when "NOTFOUND" then false
|
|
1910
|
-
else
|
|
1911
|
-
raise "Unrecognized PQMSTATUS value!"
|
|
1912
|
-
end
|
|
1913
|
-
end
|
|
1914
|
-
|
|
1915
|
-
# Pauses the given agent for this queue only. If you wish to pause this agent
|
|
1916
|
-
# for all queues, pass in :everywhere => true. Returns true if the agent was
|
|
1917
|
-
# successfully paused and false if the agent was not found.
|
|
1918
|
-
def unpause!(options={})
|
|
1919
|
-
everywhere = options.delete(:everywhere)
|
|
1920
|
-
args = [(everywhere ? nil : queue_name), interface]
|
|
1921
|
-
proxy.environment.execute('UnpauseQueueMember', *args)
|
|
1922
|
-
case proxy.environment.variable("UPQMSTATUS")
|
|
1923
|
-
when "UNPAUSED" then true
|
|
1924
|
-
when "NOTFOUND" then false
|
|
1925
|
-
else
|
|
1926
|
-
raise "Unrecognized UPQMSTATUS value!"
|
|
1927
|
-
end
|
|
1928
|
-
end
|
|
1929
|
-
|
|
1930
|
-
# Returns true/false depending on whether this agent is logged in.
|
|
1931
|
-
def logged_in?
|
|
1932
|
-
status == 'LOGGEDIN'
|
|
1933
|
-
end
|
|
1934
|
-
|
|
1935
|
-
private
|
|
1936
|
-
|
|
1937
|
-
def status
|
|
1938
|
-
agent_metadata 'status'
|
|
1939
|
-
end
|
|
1940
|
-
|
|
1941
|
-
def agent_metadata(data_name)
|
|
1942
|
-
data_name = data_name.to_s.downcase
|
|
1943
|
-
raise ArgumentError, "unrecognized agent metadata name #{data_name}" unless SUPPORTED_METADATA_NAMES.include? data_name
|
|
1944
|
-
proxy.environment.variable "AGENT(#{id}:#{data_name})"
|
|
1945
|
-
end
|
|
1946
|
-
|
|
1947
|
-
end
|
|
1948
|
-
|
|
1949
|
-
class QueueDoesNotExistError < StandardError
|
|
1950
|
-
def initialize(queue_name)
|
|
1951
|
-
super "Queue #{queue_name} does not exist!"
|
|
1952
|
-
end
|
|
1953
|
-
end
|
|
1954
|
-
|
|
1955
|
-
end
|
|
1956
|
-
|
|
1957
|
-
module MenuDigitResponse
|
|
1958
|
-
def timed_out?
|
|
1959
|
-
eql? 0.chr
|
|
1960
|
-
end
|
|
1961
|
-
end
|
|
1962
|
-
|
|
1963
|
-
end
|
|
1964
|
-
end
|
|
1965
|
-
end
|
|
1966
|
-
end
|