adhearsion 2.0.0.beta1 → 2.0.0.rc1
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/.travis.yml +2 -4
- data/CHANGELOG.md +34 -4
- data/README.markdown +2 -1
- data/Rakefile +22 -1
- data/adhearsion.gemspec +1 -0
- data/bin/ahn +0 -2
- data/features/cli_daemon.feature +2 -0
- data/features/cli_restart.feature +19 -0
- data/features/cli_start.feature +4 -6
- data/features/cli_stop.feature +3 -0
- data/features/step_definitions/app_generator_steps.rb +2 -0
- data/features/step_definitions/cli_steps.rb +2 -0
- data/features/support/aruba_helper.rb +2 -0
- data/features/support/env.rb +8 -46
- data/features/support/utils.rb +2 -0
- data/lib/adhearsion.rb +4 -6
- data/lib/adhearsion/call.rb +71 -17
- data/lib/adhearsion/call_controller.rb +25 -14
- data/lib/adhearsion/call_controller/dial.rb +34 -15
- data/lib/adhearsion/call_controller/input.rb +186 -144
- data/lib/adhearsion/call_controller/output.rb +10 -6
- data/lib/adhearsion/call_controller/record.rb +11 -13
- data/lib/adhearsion/call_controller/utility.rb +2 -0
- data/lib/adhearsion/calls.rb +4 -2
- data/lib/adhearsion/cli.rb +4 -0
- data/lib/adhearsion/cli_commands.rb +8 -2
- data/lib/adhearsion/configuration.rb +7 -3
- data/lib/adhearsion/console.rb +17 -17
- data/lib/adhearsion/events.rb +10 -4
- data/lib/adhearsion/foundation.rb +9 -0
- data/lib/adhearsion/foundation/custom_daemonizer.rb +3 -1
- data/lib/adhearsion/foundation/exception_handler.rb +2 -0
- data/lib/adhearsion/foundation/libc.rb +2 -0
- data/lib/adhearsion/foundation/object.rb +3 -0
- data/lib/adhearsion/foundation/thread_safety.rb +5 -11
- data/lib/adhearsion/generators.rb +2 -0
- data/lib/adhearsion/generators/app/app_generator.rb +2 -0
- data/lib/adhearsion/generators/app/templates/README.md +9 -0
- data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +38 -16
- data/lib/adhearsion/generators/app/templates/config/environment.rb +2 -0
- data/lib/adhearsion/generators/app/templates/lib/simon_game.rb +5 -3
- data/lib/adhearsion/generators/controller/controller_generator.rb +2 -0
- data/lib/adhearsion/generators/controller/templates/lib/controller.rb +2 -0
- data/lib/adhearsion/generators/controller/templates/spec/controller_spec.rb +2 -0
- data/lib/adhearsion/generators/generator.rb +3 -1
- data/lib/adhearsion/generators/plugin/plugin_generator.rb +2 -0
- data/lib/adhearsion/initializer.rb +31 -17
- data/lib/adhearsion/linux_proc_name.rb +2 -0
- data/lib/adhearsion/logging.rb +5 -3
- data/lib/adhearsion/menu_dsl.rb +2 -0
- data/lib/adhearsion/menu_dsl/calculated_match.rb +2 -0
- data/lib/adhearsion/menu_dsl/calculated_match_collection.rb +2 -0
- data/lib/adhearsion/menu_dsl/fixnum_match_calculator.rb +2 -0
- data/lib/adhearsion/menu_dsl/match_calculator.rb +2 -0
- data/lib/adhearsion/menu_dsl/menu.rb +58 -4
- data/lib/adhearsion/menu_dsl/menu_builder.rb +14 -1
- data/lib/adhearsion/menu_dsl/range_match_calculator.rb +4 -1
- data/lib/adhearsion/menu_dsl/string_match_calculator.rb +2 -0
- data/lib/adhearsion/outbound_call.rb +2 -0
- data/lib/adhearsion/plugin.rb +9 -7
- data/lib/adhearsion/plugin/collection.rb +3 -1
- data/lib/adhearsion/plugin/initializer.rb +3 -1
- data/lib/adhearsion/process.rb +8 -2
- data/lib/adhearsion/punchblock_plugin.rb +3 -1
- data/lib/adhearsion/punchblock_plugin/initializer.rb +34 -11
- data/lib/adhearsion/router.rb +4 -2
- data/lib/adhearsion/router/route.rb +2 -0
- data/lib/adhearsion/script_ahn_loader.rb +2 -0
- data/lib/adhearsion/tasks.rb +2 -0
- data/lib/adhearsion/tasks/configuration.rb +2 -0
- data/lib/adhearsion/tasks/debugging.rb +8 -0
- data/lib/adhearsion/tasks/environment.rb +2 -0
- data/lib/adhearsion/tasks/plugins.rb +2 -0
- data/lib/adhearsion/tasks/testing.rb +2 -0
- data/lib/adhearsion/version.rb +3 -1
- data/pre-commit +2 -0
- data/spec/adhearsion/call_controller/dial_spec.rb +114 -25
- data/spec/adhearsion/call_controller/input_spec.rb +192 -169
- data/spec/adhearsion/call_controller/output_spec.rb +26 -12
- data/spec/adhearsion/call_controller/record_spec.rb +29 -77
- data/spec/adhearsion/call_controller/utility_spec.rb +69 -0
- data/spec/adhearsion/call_controller_spec.rb +90 -15
- data/spec/adhearsion/call_spec.rb +92 -24
- data/spec/adhearsion/calls_spec.rb +9 -7
- data/spec/adhearsion/configuration_spec.rb +58 -56
- data/spec/adhearsion/console_spec.rb +4 -2
- data/spec/adhearsion/events_spec.rb +9 -7
- data/spec/adhearsion/generators_spec.rb +3 -1
- data/spec/adhearsion/initializer_spec.rb +16 -14
- data/spec/adhearsion/logging_spec.rb +11 -9
- data/spec/adhearsion/menu_dsl/calculated_match_collection_spec.rb +6 -4
- data/spec/adhearsion/menu_dsl/calculated_match_spec.rb +6 -4
- data/spec/adhearsion/menu_dsl/fixnum_match_calculator_spec.rb +3 -1
- data/spec/adhearsion/menu_dsl/match_calculator_spec.rb +2 -0
- data/spec/adhearsion/menu_dsl/menu_builder_spec.rb +42 -11
- data/spec/adhearsion/menu_dsl/menu_spec.rb +197 -36
- data/spec/adhearsion/menu_dsl/range_match_calculator_spec.rb +4 -2
- data/spec/adhearsion/menu_dsl/string_match_calculator_spec.rb +5 -3
- data/spec/adhearsion/outbound_call_spec.rb +7 -5
- data/spec/adhearsion/plugin_spec.rb +19 -15
- data/spec/adhearsion/process_spec.rb +12 -7
- data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +35 -15
- data/spec/adhearsion/punchblock_plugin_spec.rb +4 -1
- data/spec/adhearsion/router/route_spec.rb +8 -6
- data/spec/adhearsion/router_spec.rb +12 -10
- data/spec/adhearsion_spec.rb +13 -2
- data/spec/capture_warnings.rb +33 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/call_controller_test_helpers.rb +2 -4
- data/spec/support/initializer_stubs.rb +8 -5
- data/spec/support/logging_helpers.rb +2 -0
- data/spec/support/punchblock_mocks.rb +2 -0
- metadata +84 -71
- data/EVENTS +0 -11
- data/lib/adhearsion/call_controller/menu.rb +0 -124
- data/lib/adhearsion/foundation/all.rb +0 -8
- data/spec/adhearsion/call_controller/menu_spec.rb +0 -120
- data/spec/adhearsion/menu_dsl_spec.rb +0 -12
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Adhearsion
|
2
4
|
class CallController
|
3
5
|
extend ActiveSupport::Autoload
|
@@ -6,14 +8,12 @@ module Adhearsion
|
|
6
8
|
autoload :Input
|
7
9
|
autoload :Output
|
8
10
|
autoload :Record
|
9
|
-
autoload :Menu
|
10
11
|
autoload :Utility
|
11
12
|
|
12
13
|
include Dial
|
13
14
|
include Input
|
14
15
|
include Output
|
15
16
|
include Record
|
16
|
-
include Menu
|
17
17
|
include Utility
|
18
18
|
|
19
19
|
class_attribute :callbacks
|
@@ -63,16 +63,16 @@ module Adhearsion
|
|
63
63
|
call.register_controller! self
|
64
64
|
execute_callbacks :before_call
|
65
65
|
run
|
66
|
-
rescue Hangup
|
66
|
+
rescue Call::Hangup
|
67
67
|
logger.info "Call was hung up"
|
68
68
|
rescue SyntaxError, StandardError => e
|
69
|
-
Events.trigger :exception, e
|
69
|
+
Events.trigger :exception, [e, logger]
|
70
70
|
ensure
|
71
71
|
after_call
|
72
72
|
end
|
73
73
|
|
74
74
|
def run
|
75
|
-
instance_exec
|
75
|
+
instance_exec(&block) if block
|
76
76
|
end
|
77
77
|
|
78
78
|
def invoke(controller_class, metadata = nil)
|
@@ -87,7 +87,7 @@ module Adhearsion
|
|
87
87
|
def execute_callbacks(type) # :nodoc:
|
88
88
|
self.class.callbacks[type].each do |callback|
|
89
89
|
catching_standard_errors do
|
90
|
-
instance_exec
|
90
|
+
instance_exec(&callback)
|
91
91
|
end
|
92
92
|
end
|
93
93
|
end
|
@@ -113,37 +113,48 @@ module Adhearsion
|
|
113
113
|
yield component if block_given?
|
114
114
|
|
115
115
|
complete_event = component.complete_event
|
116
|
-
raise StandardError, complete_event.reason.details if complete_event.reason.is_a? Punchblock::Event::Complete::Error
|
116
|
+
raise StandardError, [complete_event.reason.details, component.inspect].join(": ") if complete_event.reason.is_a? Punchblock::Event::Complete::Error
|
117
117
|
component
|
118
118
|
end
|
119
119
|
|
120
120
|
def answer(*args)
|
121
121
|
block_until_resumed
|
122
|
-
call.answer
|
122
|
+
call.answer(*args)
|
123
123
|
end
|
124
124
|
|
125
125
|
def reject(*args)
|
126
126
|
block_until_resumed
|
127
|
-
call.reject
|
127
|
+
call.reject(*args)
|
128
128
|
end
|
129
129
|
|
130
130
|
def mute(*args)
|
131
131
|
block_until_resumed
|
132
|
-
call.mute
|
132
|
+
call.mute(*args)
|
133
133
|
end
|
134
134
|
|
135
135
|
def unmute(*args)
|
136
136
|
block_until_resumed
|
137
|
-
call.unmute
|
137
|
+
call.unmute(*args)
|
138
138
|
end
|
139
139
|
|
140
|
-
def join(
|
140
|
+
def join(target, options = {})
|
141
|
+
async = if target.is_a?(Hash)
|
142
|
+
target.delete :async
|
143
|
+
else
|
144
|
+
options.delete :async
|
145
|
+
end
|
141
146
|
block_until_resumed
|
142
|
-
call.join
|
147
|
+
join_command = call.join target, options
|
148
|
+
waiter = join_command.other_call_id || join_command.mixer_name
|
149
|
+
if async
|
150
|
+
call.wait_for_joined waiter
|
151
|
+
else
|
152
|
+
call.wait_for_unjoined waiter
|
153
|
+
end
|
143
154
|
end
|
144
155
|
|
145
156
|
def block_until_resumed # :nodoc:
|
146
|
-
|
157
|
+
instance_variable_defined?(:@pause_latch) && @pause_latch.wait
|
147
158
|
end
|
148
159
|
|
149
160
|
def pause! # :nodoc:
|
@@ -1,15 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Adhearsion
|
2
4
|
class CallController
|
3
5
|
module Dial
|
4
6
|
#
|
5
7
|
# Dial a third party and join to this call
|
6
8
|
#
|
7
|
-
# @param [String|Array<String
|
9
|
+
# @param [String|Array<String>|Hash] number represents the extension or "number" that asterisk should dial.
|
8
10
|
# Be careful to not just specify a number like 5001, 9095551001
|
9
11
|
# You must specify a properly formatted string as Asterisk would expect to use in order to understand
|
10
12
|
# whether the call should be dialed using SIP, IAX, or some other means.
|
11
13
|
# You can also specify an array of destinations: each will be called with the same options simultaneously.
|
12
14
|
# The first call answered is joined, the others are hung up.
|
15
|
+
# A hash argument has the dial target as each key, and an hash of options as the value, in the form:
|
16
|
+
# dial({'SIP/100' => {:timeout => 3000}, 'SIP/200' => {:timeout => 4000} })
|
17
|
+
# The option hash for each target is merged into the global options, overriding them for the single dial.
|
18
|
+
# Destinations are dialed simultaneously as with an array.
|
13
19
|
#
|
14
20
|
# @param [Hash] options
|
15
21
|
#
|
@@ -31,7 +37,8 @@ module Adhearsion
|
|
31
37
|
# dial "IAX2/my.id@voipjet/19095551234", :from => "John Doe <9095551234>"
|
32
38
|
#
|
33
39
|
def dial(to, options = {}, latch = nil)
|
34
|
-
targets = Array(to)
|
40
|
+
targets = to.respond_to?(:has_key?) ? to : Array(to)
|
41
|
+
|
35
42
|
status = DialStatus.new
|
36
43
|
|
37
44
|
latch ||= CountDownLatch.new targets.size
|
@@ -43,40 +50,46 @@ module Adhearsion
|
|
43
50
|
|
44
51
|
options[:from] ||= call.from
|
45
52
|
|
46
|
-
calls = targets.map do |target|
|
53
|
+
calls = targets.map do |target, specific_options|
|
47
54
|
new_call = OutboundCall.new
|
48
55
|
|
49
56
|
new_call.on_answer do |event|
|
50
|
-
calls.each do |call_to_hangup,
|
57
|
+
calls.each do |call_to_hangup, _|
|
51
58
|
begin
|
52
59
|
next if call_to_hangup.id == new_call.id
|
53
|
-
logger.debug "
|
60
|
+
logger.debug "#dial hanging up call #{call_to_hangup.id} because this call has been answered by another channel"
|
54
61
|
call_to_hangup.hangup
|
55
62
|
rescue Celluloid::DeadActorError
|
56
63
|
# This actor may previously have been shut down due to the call ending
|
57
64
|
end
|
58
65
|
end
|
59
66
|
|
60
|
-
new_call.register_event_handler Punchblock::Event::Unjoined, :other_call_id => call.id do |
|
67
|
+
new_call.register_event_handler Punchblock::Event::Unjoined, :other_call_id => call.id do |unjoined|
|
61
68
|
new_call["dial_countdown_#{call.id}"] = true
|
62
69
|
latch.countdown!
|
63
70
|
throw :pass
|
64
71
|
end
|
65
72
|
|
66
|
-
logger.debug "
|
73
|
+
logger.debug "#dial joining call #{new_call.id} to #{call.id}"
|
67
74
|
new_call.join call
|
68
75
|
status.answer!
|
69
76
|
end
|
70
77
|
|
71
78
|
new_call.on_end do |event|
|
72
79
|
latch.countdown! unless new_call["dial_countdown_#{call.id}"]
|
80
|
+
|
81
|
+
case event.reason
|
82
|
+
when :error
|
83
|
+
status.error!
|
84
|
+
end
|
73
85
|
end
|
74
86
|
|
75
|
-
[new_call, target]
|
87
|
+
[new_call, target, specific_options]
|
76
88
|
end
|
77
89
|
|
78
|
-
calls.map! do |call, target|
|
79
|
-
|
90
|
+
calls.map! do |call, target, specific_options|
|
91
|
+
local_options = options.dup.deep_merge specific_options if specific_options
|
92
|
+
call.dial target, (local_options || options)
|
80
93
|
call
|
81
94
|
end
|
82
95
|
|
@@ -85,10 +98,9 @@ module Adhearsion
|
|
85
98
|
no_timeout = latch.wait options[:timeout]
|
86
99
|
status.timeout! unless no_timeout
|
87
100
|
|
88
|
-
logger.debug "#dial finished. Hanging up #{calls.size} outbound calls #{calls.
|
101
|
+
logger.debug "#dial finished. Hanging up #{calls.size} outbound calls: #{calls.map(&:id).join ", "}."
|
89
102
|
calls.each do |outbound_call|
|
90
103
|
begin
|
91
|
-
logger.debug "Hanging up #{outbound_call} because the #dial that created it is complete."
|
92
104
|
outbound_call.hangup
|
93
105
|
rescue Celluloid::DeadActorError
|
94
106
|
# This actor may previously have been shut down due to the call ending
|
@@ -100,10 +112,13 @@ module Adhearsion
|
|
100
112
|
|
101
113
|
class DialStatus
|
102
114
|
attr_accessor :calls
|
103
|
-
attr_reader :result
|
104
115
|
|
105
116
|
def initialize
|
106
|
-
@result =
|
117
|
+
@result = nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def result
|
121
|
+
@result || :no_answer
|
107
122
|
end
|
108
123
|
|
109
124
|
def answer!
|
@@ -111,7 +126,11 @@ module Adhearsion
|
|
111
126
|
end
|
112
127
|
|
113
128
|
def timeout!
|
114
|
-
@result
|
129
|
+
@result ||= :timeout
|
130
|
+
end
|
131
|
+
|
132
|
+
def error!
|
133
|
+
@result ||= :error
|
115
134
|
end
|
116
135
|
end
|
117
136
|
|
@@ -1,6 +1,186 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Adhearsion
|
2
4
|
class CallController
|
3
5
|
module Input
|
6
|
+
|
7
|
+
Result = Struct.new(:response, :status, :menu) do
|
8
|
+
def to_s
|
9
|
+
response
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Prompts for input via DTMF, handling playback of prompts,
|
14
|
+
# timeouts, digit limits and terminator digits.
|
15
|
+
#
|
16
|
+
# @example A basic digit collection:
|
17
|
+
# ask "Welcome, ", "/opt/sounds/menu-prompt.mp3",
|
18
|
+
# :timeout => 10, :terminator => '#', :limit => 3 do |buffer|
|
19
|
+
# buffer == "12980"
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# The first arguments will be a list of sounds to play, as accepted by #play, including strings for TTS, Date and Time objects, and file paths.
|
23
|
+
# :timeout, :terminator and :limit options may then be specified.
|
24
|
+
# A block may be passed which is invoked on each digit being collected. If it returns true, the collection is terminated.
|
25
|
+
#
|
26
|
+
# @param [Object] A list of outputs to play, as accepted by #play
|
27
|
+
# @param [Hash] options Options to use for the menu
|
28
|
+
# @option options [Boolean] :interruptible If the prompt should be interruptible or not. Defaults to true
|
29
|
+
# @option options [Integer] :limit Digit limit (causes collection to cease after a specified number of digits have been collected)
|
30
|
+
# @option options [Integer] :timeout Timeout in seconds before the first and between each input digit
|
31
|
+
# @option options [String] :terminator Digit to terminate input
|
32
|
+
#
|
33
|
+
# @return [Result] a result object from which the #response and #status may be established
|
34
|
+
#
|
35
|
+
# @see play
|
36
|
+
# @see pass
|
37
|
+
#
|
38
|
+
def ask(*args, &block)
|
39
|
+
options = args.last.kind_of?(Hash) ? args.pop : {}
|
40
|
+
sound_files = args.flatten
|
41
|
+
|
42
|
+
menu_instance = MenuDSL::Menu.new options do
|
43
|
+
validator(&block) if block
|
44
|
+
end
|
45
|
+
menu_instance.validate :basic
|
46
|
+
result_of_menu = nil
|
47
|
+
|
48
|
+
until MenuDSL::Menu::MenuResultDone === result_of_menu
|
49
|
+
result_of_menu = menu_instance.continue
|
50
|
+
|
51
|
+
if result_of_menu.is_a?(MenuDSL::Menu::MenuGetAnotherDigit)
|
52
|
+
next_digit = play_sound_files_for_menu menu_instance, sound_files
|
53
|
+
menu_instance << next_digit if next_digit
|
54
|
+
end # case
|
55
|
+
end # while
|
56
|
+
|
57
|
+
Result.new.tap do |result|
|
58
|
+
result.response = menu_instance.result
|
59
|
+
result.status = menu_instance.status
|
60
|
+
result.menu = menu_instance
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Creates and manages a multiple choice menu driven by DTMF, handling playback of prompts,
|
65
|
+
# invalid input, retries and timeouts, and final failures.
|
66
|
+
#
|
67
|
+
# @example A complete example of the method is as follows:
|
68
|
+
# ask "Welcome, ", "/opt/sounds/menu-prompt.mp3", :tries => 2, :timeout => 10 do
|
69
|
+
# match 1, OperatorController
|
70
|
+
#
|
71
|
+
# match 10..19 do
|
72
|
+
# pass DirectController
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# match 5, 6, 9 do |exten|
|
76
|
+
# play "The #{exten} extension is currently not active"
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# match '7', OfficeController
|
80
|
+
#
|
81
|
+
# invalid { play "Please choose a valid extension" }
|
82
|
+
# timeout { play "Input timed out, try again." }
|
83
|
+
# failure { pass OperatorController }
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# The first arguments will be a list of sounds to play, as accepted by #play, including strings for TTS, Date and Time objects, and file paths.
|
87
|
+
# :tries and :timeout options respectively specify the number of tries before going into failure, and the timeout in seconds allowed on each digit input.
|
88
|
+
# The most important part is the following block, which specifies how the menu will be constructed and handled.
|
89
|
+
#
|
90
|
+
# #match handles connecting an input pattern to a payload.
|
91
|
+
# The pattern can be one or more of: an integer, a Range, a string, an Array of the possible single types.
|
92
|
+
# Input is matched against patterns, and the first exact match has it's payload executed.
|
93
|
+
# Matched input is passed in to the associated block, or to the controller through #options.
|
94
|
+
#
|
95
|
+
# Allowed payloads are the name of a controller class, in which case it is executed through its #run method, or a block.
|
96
|
+
#
|
97
|
+
# #invalid has its associated block executed when the input does not possibly match any pattern.
|
98
|
+
# #timeout's block is run when time expires before or between input digits.
|
99
|
+
# #failure runs its block when the maximum number of tries is reached without an input match.
|
100
|
+
#
|
101
|
+
# #validator runs its block on each digit being collected. If it returns true, the collection is terminated.
|
102
|
+
#
|
103
|
+
# Execution of the current context resumes after #ask finishes. If you wish to jump to an entirely different controller, use #pass.
|
104
|
+
# Menu will return :failed if failure was reached, or :done if a match was executed.
|
105
|
+
#
|
106
|
+
# @param [Object] A list of outputs to play, as accepted by #play
|
107
|
+
# @param [Hash] options Options to use for the menu
|
108
|
+
# @option options [Integer] :tries Number of tries allowed before failure
|
109
|
+
# @option options [Integer] :timeout Timeout in seconds before the first and between each input digit
|
110
|
+
# @option options [Boolean] :interruptible If the prompt should be interruptible or not. Defaults to true
|
111
|
+
#
|
112
|
+
# @return [Result] a result object from which the #response and #status may be established
|
113
|
+
#
|
114
|
+
# @see play
|
115
|
+
# @see pass
|
116
|
+
#
|
117
|
+
def menu(*args, &block)
|
118
|
+
options = args.last.kind_of?(Hash) ? args.pop : {}
|
119
|
+
sound_files = args.flatten
|
120
|
+
|
121
|
+
menu_instance = MenuDSL::Menu.new options, &block
|
122
|
+
menu_instance.validate
|
123
|
+
result_of_menu = nil
|
124
|
+
|
125
|
+
catch :finish do
|
126
|
+
until MenuDSL::Menu::MenuResultDone === result_of_menu
|
127
|
+
if menu_instance.should_continue?
|
128
|
+
result_of_menu = menu_instance.continue
|
129
|
+
else
|
130
|
+
logger.debug "Menu failed to get valid input. Calling \"failure\" hook."
|
131
|
+
menu_instance.execute_failure_hook
|
132
|
+
throw :finish
|
133
|
+
end
|
134
|
+
|
135
|
+
case result_of_menu
|
136
|
+
when MenuDSL::Menu::MenuResultInvalid
|
137
|
+
logger.debug "Menu received invalid input. Calling \"invalid\" hook and restarting."
|
138
|
+
menu_instance.execute_invalid_hook
|
139
|
+
menu_instance.restart!
|
140
|
+
result_of_menu = nil
|
141
|
+
when MenuDSL::Menu::MenuGetAnotherDigit
|
142
|
+
next_digit = play_sound_files_for_menu menu_instance, sound_files
|
143
|
+
if next_digit
|
144
|
+
menu_instance << next_digit
|
145
|
+
else
|
146
|
+
case result_of_menu
|
147
|
+
when MenuDSL::Menu::MenuGetAnotherDigitOrFinish
|
148
|
+
jump_to result_of_menu.match_object, :extension => result_of_menu.new_extension
|
149
|
+
throw :finish
|
150
|
+
when MenuDSL::Menu::MenuGetAnotherDigitOrTimeout
|
151
|
+
logger.debug "Menu timed out. Calling \"timeout\" hook and restarting."
|
152
|
+
menu_instance.execute_timeout_hook
|
153
|
+
menu_instance.restart!
|
154
|
+
result_of_menu = nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
when MenuDSL::Menu::MenuResultFound
|
158
|
+
logger.debug "Menu received valid input (#{result_of_menu.new_extension}). Calling the matching hook."
|
159
|
+
jump_to result_of_menu.match_object, :extension => result_of_menu.new_extension
|
160
|
+
throw :finish
|
161
|
+
end # case
|
162
|
+
end # while
|
163
|
+
end
|
164
|
+
|
165
|
+
Result.new.tap do |result|
|
166
|
+
result.response = menu_instance.result
|
167
|
+
result.status = menu_instance.status
|
168
|
+
result.menu = menu_instance
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def play_sound_files_for_menu(menu_instance, sound_files) # :nodoc:
|
173
|
+
digit = nil
|
174
|
+
if sound_files.any? && menu_instance.digit_buffer_empty?
|
175
|
+
if menu_instance.interruptible
|
176
|
+
digit = interruptible_play(*sound_files)
|
177
|
+
else
|
178
|
+
play(*sound_files)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
digit || wait_for_digit(menu_instance.timeout)
|
182
|
+
end
|
183
|
+
|
4
184
|
#
|
5
185
|
# Waits for a single digit and returns it, or returns nil if nothing was pressed
|
6
186
|
#
|
@@ -22,152 +202,14 @@ module Adhearsion
|
|
22
202
|
parse_single_dtmf result
|
23
203
|
end
|
24
204
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
# 1. The number of digits you specify as the first argument is collected
|
29
|
-
# 2. The timeout you specify with the :timeout option elapses, in seconds.
|
30
|
-
# 3. The "#" key (or the key you specify with :accept_key) is pressed
|
31
|
-
#
|
32
|
-
# Usage examples
|
33
|
-
#
|
34
|
-
# input # Receives digits until the caller presses the "#" key
|
35
|
-
# input 3 # Receives three digits. Can be 0-9, * or #
|
36
|
-
# input 5, :accept_key => "*" # Receive at most 5 digits, stopping if '*' is pressed
|
37
|
-
# input 1, :timeout => 60000 # Receive a single digit, returning an empty
|
38
|
-
# string if the timeout is encountered
|
39
|
-
# input 9, :timeout => 7000, :accept_key => "0" # Receives nine digits, returning
|
40
|
-
# # when the timeout is encountered
|
41
|
-
# # or when the "0" key is pressed.
|
42
|
-
# input 3, :play => "you-sound-cute"
|
43
|
-
# input :play => ["if-this-is-correct-press", 1, "otherwise-press", 2]
|
44
|
-
# input :interruptible => false, :play => ["you-cannot-interrupt-this-message"] # Disallow DTMF (keypad) interruption
|
45
|
-
# # until after all files are played.
|
46
|
-
#
|
47
|
-
# When specifying outputs to play, the playback of the sequence of files will stop
|
48
|
-
# immediately when the user presses the first digit.
|
49
|
-
#
|
50
|
-
# Accepted output types are:
|
51
|
-
# 1. Any object supported by detect_type (@see detect_type)
|
52
|
-
# 2. Any valid SSML document
|
53
|
-
# 3. An Hash with at least the :value key set to a supported object type, and other keys as options to the specific output
|
54
|
-
#
|
55
|
-
# :play usage examples
|
56
|
-
# input 1, :play => RubySpeech::SSML.draw { string "hello there" } # 1 digit, SSML document
|
57
|
-
# input 2, :play => "hello there" # 2 digits, string
|
58
|
-
# input 2, :play => {:value => Time.now, :strftime => "%H:%M"} # 2 digits, Hash with :value
|
59
|
-
# input :play => [ "the time is", {:value => Time.now, :strftime => "%H:%M"} ] # no digit limit, two mixed outputs
|
60
|
-
#
|
61
|
-
# The :timeout option works like a digit timeout, therefore each digit pressed
|
62
|
-
# causes the timer to reset. This is a much more user-friendly approach than an
|
63
|
-
# absolute timeout.
|
64
|
-
#
|
65
|
-
# Note that when the digit limit is not specified the :accept_key becomes "#".
|
66
|
-
# Otherwise there would be no way to end the collection of digits. You can
|
67
|
-
# obviously override this by passing in a new key with :accept_key.
|
68
|
-
#
|
69
|
-
# @return [String] The keypad input received. An empty string is returned in the
|
70
|
-
# absense of input. If the :accept_key argument was pressed, it
|
71
|
-
# will not appear in the output.
|
72
|
-
def input(*args, &block)
|
73
|
-
begin
|
74
|
-
input! *args, &block
|
75
|
-
rescue PlaybackError => e
|
76
|
-
logger.warn { e }
|
77
|
-
retry # If sound playback fails, play the remaining sound files and wait for digits
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
# Same as {#input}, but immediately raises an exception if sound playback fails
|
82
|
-
#
|
83
|
-
# @return (see #input)
|
84
|
-
# @raise [Adhearsion::PlaybackError] If a sound file cannot be played
|
85
|
-
def input!(*args, &block)
|
86
|
-
options = args.last.kind_of?(Hash) ? args.pop : {}
|
87
|
-
number_of_digits = args.shift
|
88
|
-
|
89
|
-
options[:play] = Array(case options[:play]
|
90
|
-
when String
|
91
|
-
options[:play]
|
92
|
-
when Array
|
93
|
-
options[:play].compact
|
94
|
-
when NilClass
|
95
|
-
[]
|
96
|
-
else
|
97
|
-
[options[:play]]
|
98
|
-
end)
|
99
|
-
|
100
|
-
play_command = if options.has_key?(:interruptible) && options[:interruptible] == false
|
101
|
-
:play!
|
205
|
+
def jump_to(match_object, overrides = nil) # :nodoc:
|
206
|
+
if match_object.block
|
207
|
+
instance_exec overrides[:extension], &match_object.block
|
102
208
|
else
|
103
|
-
|
104
|
-
:interruptible_play!
|
105
|
-
end
|
106
|
-
|
107
|
-
if options.has_key? :speak
|
108
|
-
raise ArgumentError, ':speak must be a Hash' unless options[:speak].is_a? Hash
|
109
|
-
raise ArgumentError, 'Must include a text string when requesting TTS fallback' unless options[:speak].has_key?(:text)
|
110
|
-
if options.has_key?(:speak) && options.has_key?(:play) && options[:play].size > 0
|
111
|
-
raise ArgumentError, 'Must specify only one of :play or :speak'
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
timeout = options[:timeout]
|
116
|
-
terminator = options[:terminator]
|
117
|
-
|
118
|
-
terminator = if terminator
|
119
|
-
terminator.to_s
|
120
|
-
elsif number_of_digits.nil? && !terminator.equal?(false)
|
121
|
-
'#'
|
122
|
-
end
|
123
|
-
|
124
|
-
if number_of_digits && number_of_digits < 0
|
125
|
-
logger.warn "Giving -1 to #input is now deprecated. Do not specify a first " +
|
126
|
-
"argument to allow unlimited digits." if number_of_digits == -1
|
127
|
-
raise ArgumentError, "The number of digits must be positive!"
|
128
|
-
end
|
129
|
-
|
130
|
-
buffer = ''
|
131
|
-
if options[:play].any?
|
132
|
-
# Consume the sound files one at a time. In the event of playback
|
133
|
-
# failure, this tells us which files remain unplayed.
|
134
|
-
while output = options[:play].shift
|
135
|
-
if output.class == Hash
|
136
|
-
argument = output.delete(:value)
|
137
|
-
raise ArgumentError, ':value has to be specified for each :play argument that is a Hash' if argument.nil?
|
138
|
-
output = [argument, output]
|
139
|
-
end
|
140
|
-
key = send play_command, output
|
141
|
-
key = nil if play_command == :play!
|
142
|
-
break if key
|
143
|
-
end
|
144
|
-
key ||= ''
|
145
|
-
# instead use a normal play command, :speak is basically an alias
|
146
|
-
elsif options[:speak]
|
147
|
-
speak_output = options[:speak].delete(:text)
|
148
|
-
key = send play_command, speak_output, options[:speak]
|
149
|
-
key = nil if play_command == :play!
|
150
|
-
else
|
151
|
-
key = wait_for_digit timeout
|
209
|
+
invoke match_object.match_payload, overrides
|
152
210
|
end
|
211
|
+
end
|
153
212
|
|
154
|
-
|
155
|
-
return buffer if key.nil?
|
156
|
-
if terminator
|
157
|
-
if key == terminator
|
158
|
-
return buffer
|
159
|
-
else
|
160
|
-
buffer << key
|
161
|
-
return buffer if number_of_digits && number_of_digits == buffer.length
|
162
|
-
end
|
163
|
-
else
|
164
|
-
buffer << key
|
165
|
-
return buffer if number_of_digits && number_of_digits == buffer.length
|
166
|
-
end
|
167
|
-
return buffer if block_given? && yield(buffer)
|
168
|
-
key = wait_for_digit timeout
|
169
|
-
end
|
170
|
-
end # #input!
|
171
|
-
end # Input
|
213
|
+
end # module
|
172
214
|
end
|
173
215
|
end
|