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.
Files changed (118) hide show
  1. data/.travis.yml +2 -4
  2. data/CHANGELOG.md +34 -4
  3. data/README.markdown +2 -1
  4. data/Rakefile +22 -1
  5. data/adhearsion.gemspec +1 -0
  6. data/bin/ahn +0 -2
  7. data/features/cli_daemon.feature +2 -0
  8. data/features/cli_restart.feature +19 -0
  9. data/features/cli_start.feature +4 -6
  10. data/features/cli_stop.feature +3 -0
  11. data/features/step_definitions/app_generator_steps.rb +2 -0
  12. data/features/step_definitions/cli_steps.rb +2 -0
  13. data/features/support/aruba_helper.rb +2 -0
  14. data/features/support/env.rb +8 -46
  15. data/features/support/utils.rb +2 -0
  16. data/lib/adhearsion.rb +4 -6
  17. data/lib/adhearsion/call.rb +71 -17
  18. data/lib/adhearsion/call_controller.rb +25 -14
  19. data/lib/adhearsion/call_controller/dial.rb +34 -15
  20. data/lib/adhearsion/call_controller/input.rb +186 -144
  21. data/lib/adhearsion/call_controller/output.rb +10 -6
  22. data/lib/adhearsion/call_controller/record.rb +11 -13
  23. data/lib/adhearsion/call_controller/utility.rb +2 -0
  24. data/lib/adhearsion/calls.rb +4 -2
  25. data/lib/adhearsion/cli.rb +4 -0
  26. data/lib/adhearsion/cli_commands.rb +8 -2
  27. data/lib/adhearsion/configuration.rb +7 -3
  28. data/lib/adhearsion/console.rb +17 -17
  29. data/lib/adhearsion/events.rb +10 -4
  30. data/lib/adhearsion/foundation.rb +9 -0
  31. data/lib/adhearsion/foundation/custom_daemonizer.rb +3 -1
  32. data/lib/adhearsion/foundation/exception_handler.rb +2 -0
  33. data/lib/adhearsion/foundation/libc.rb +2 -0
  34. data/lib/adhearsion/foundation/object.rb +3 -0
  35. data/lib/adhearsion/foundation/thread_safety.rb +5 -11
  36. data/lib/adhearsion/generators.rb +2 -0
  37. data/lib/adhearsion/generators/app/app_generator.rb +2 -0
  38. data/lib/adhearsion/generators/app/templates/README.md +9 -0
  39. data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +38 -16
  40. data/lib/adhearsion/generators/app/templates/config/environment.rb +2 -0
  41. data/lib/adhearsion/generators/app/templates/lib/simon_game.rb +5 -3
  42. data/lib/adhearsion/generators/controller/controller_generator.rb +2 -0
  43. data/lib/adhearsion/generators/controller/templates/lib/controller.rb +2 -0
  44. data/lib/adhearsion/generators/controller/templates/spec/controller_spec.rb +2 -0
  45. data/lib/adhearsion/generators/generator.rb +3 -1
  46. data/lib/adhearsion/generators/plugin/plugin_generator.rb +2 -0
  47. data/lib/adhearsion/initializer.rb +31 -17
  48. data/lib/adhearsion/linux_proc_name.rb +2 -0
  49. data/lib/adhearsion/logging.rb +5 -3
  50. data/lib/adhearsion/menu_dsl.rb +2 -0
  51. data/lib/adhearsion/menu_dsl/calculated_match.rb +2 -0
  52. data/lib/adhearsion/menu_dsl/calculated_match_collection.rb +2 -0
  53. data/lib/adhearsion/menu_dsl/fixnum_match_calculator.rb +2 -0
  54. data/lib/adhearsion/menu_dsl/match_calculator.rb +2 -0
  55. data/lib/adhearsion/menu_dsl/menu.rb +58 -4
  56. data/lib/adhearsion/menu_dsl/menu_builder.rb +14 -1
  57. data/lib/adhearsion/menu_dsl/range_match_calculator.rb +4 -1
  58. data/lib/adhearsion/menu_dsl/string_match_calculator.rb +2 -0
  59. data/lib/adhearsion/outbound_call.rb +2 -0
  60. data/lib/adhearsion/plugin.rb +9 -7
  61. data/lib/adhearsion/plugin/collection.rb +3 -1
  62. data/lib/adhearsion/plugin/initializer.rb +3 -1
  63. data/lib/adhearsion/process.rb +8 -2
  64. data/lib/adhearsion/punchblock_plugin.rb +3 -1
  65. data/lib/adhearsion/punchblock_plugin/initializer.rb +34 -11
  66. data/lib/adhearsion/router.rb +4 -2
  67. data/lib/adhearsion/router/route.rb +2 -0
  68. data/lib/adhearsion/script_ahn_loader.rb +2 -0
  69. data/lib/adhearsion/tasks.rb +2 -0
  70. data/lib/adhearsion/tasks/configuration.rb +2 -0
  71. data/lib/adhearsion/tasks/debugging.rb +8 -0
  72. data/lib/adhearsion/tasks/environment.rb +2 -0
  73. data/lib/adhearsion/tasks/plugins.rb +2 -0
  74. data/lib/adhearsion/tasks/testing.rb +2 -0
  75. data/lib/adhearsion/version.rb +3 -1
  76. data/pre-commit +2 -0
  77. data/spec/adhearsion/call_controller/dial_spec.rb +114 -25
  78. data/spec/adhearsion/call_controller/input_spec.rb +192 -169
  79. data/spec/adhearsion/call_controller/output_spec.rb +26 -12
  80. data/spec/adhearsion/call_controller/record_spec.rb +29 -77
  81. data/spec/adhearsion/call_controller/utility_spec.rb +69 -0
  82. data/spec/adhearsion/call_controller_spec.rb +90 -15
  83. data/spec/adhearsion/call_spec.rb +92 -24
  84. data/spec/adhearsion/calls_spec.rb +9 -7
  85. data/spec/adhearsion/configuration_spec.rb +58 -56
  86. data/spec/adhearsion/console_spec.rb +4 -2
  87. data/spec/adhearsion/events_spec.rb +9 -7
  88. data/spec/adhearsion/generators_spec.rb +3 -1
  89. data/spec/adhearsion/initializer_spec.rb +16 -14
  90. data/spec/adhearsion/logging_spec.rb +11 -9
  91. data/spec/adhearsion/menu_dsl/calculated_match_collection_spec.rb +6 -4
  92. data/spec/adhearsion/menu_dsl/calculated_match_spec.rb +6 -4
  93. data/spec/adhearsion/menu_dsl/fixnum_match_calculator_spec.rb +3 -1
  94. data/spec/adhearsion/menu_dsl/match_calculator_spec.rb +2 -0
  95. data/spec/adhearsion/menu_dsl/menu_builder_spec.rb +42 -11
  96. data/spec/adhearsion/menu_dsl/menu_spec.rb +197 -36
  97. data/spec/adhearsion/menu_dsl/range_match_calculator_spec.rb +4 -2
  98. data/spec/adhearsion/menu_dsl/string_match_calculator_spec.rb +5 -3
  99. data/spec/adhearsion/outbound_call_spec.rb +7 -5
  100. data/spec/adhearsion/plugin_spec.rb +19 -15
  101. data/spec/adhearsion/process_spec.rb +12 -7
  102. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +35 -15
  103. data/spec/adhearsion/punchblock_plugin_spec.rb +4 -1
  104. data/spec/adhearsion/router/route_spec.rb +8 -6
  105. data/spec/adhearsion/router_spec.rb +12 -10
  106. data/spec/adhearsion_spec.rb +13 -2
  107. data/spec/capture_warnings.rb +33 -0
  108. data/spec/spec_helper.rb +4 -0
  109. data/spec/support/call_controller_test_helpers.rb +2 -4
  110. data/spec/support/initializer_stubs.rb +8 -5
  111. data/spec/support/logging_helpers.rb +2 -0
  112. data/spec/support/punchblock_mocks.rb +2 -0
  113. metadata +84 -71
  114. data/EVENTS +0 -11
  115. data/lib/adhearsion/call_controller/menu.rb +0 -124
  116. data/lib/adhearsion/foundation/all.rb +0 -8
  117. data/spec/adhearsion/call_controller/menu_spec.rb +0 -120
  118. 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 &block if block
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 &callback
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 *args
122
+ call.answer(*args)
123
123
  end
124
124
 
125
125
  def reject(*args)
126
126
  block_until_resumed
127
- call.reject *args
127
+ call.reject(*args)
128
128
  end
129
129
 
130
130
  def mute(*args)
131
131
  block_until_resumed
132
- call.mute *args
132
+ call.mute(*args)
133
133
  end
134
134
 
135
135
  def unmute(*args)
136
136
  block_until_resumed
137
- call.unmute *args
137
+ call.unmute(*args)
138
138
  end
139
139
 
140
- def join(*args)
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 *args
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
- @pause_latch && @pause_latch.wait
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>] number represents the extension or "number" that asterisk should dial.
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, target|
57
+ calls.each do |call_to_hangup, _|
51
58
  begin
52
59
  next if call_to_hangup.id == new_call.id
53
- logger.debug "Hanging up call #{call_to_hangup.id} because it was not the first to answer a #dial"
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 |event|
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 "Joining call #{new_call.id} to #{call.id} due to a #dial"
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
- call.dial target, options
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.inspect}."
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 = :no_answer
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 = :timeout
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
- # Used to receive keypad input from the user. Digits are collected
26
- # via DTMF (keypad) input until one of three things happens:
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
- options[:interruptible] = true
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
- loop do
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