adhearsion 2.0.0.rc5 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -0
- data/CHANGELOG.md +28 -126
- data/LICENSE +20 -456
- data/README.markdown +20 -29
- data/adhearsion.gemspec +22 -31
- data/lib/adhearsion.rb +5 -2
- data/lib/adhearsion/call.rb +57 -20
- data/lib/adhearsion/call_controller.rb +83 -13
- data/lib/adhearsion/call_controller/dial.rb +35 -20
- data/lib/adhearsion/call_controller/input.rb +20 -15
- data/lib/adhearsion/call_controller/menu_dsl.rb +1 -0
- data/lib/adhearsion/call_controller/output.rb +53 -42
- data/lib/adhearsion/call_controller/record.rb +3 -3
- data/lib/adhearsion/call_controller/utility.rb +11 -5
- data/lib/adhearsion/cli_commands.rb +2 -1
- data/lib/adhearsion/generators.rb +1 -1
- data/lib/adhearsion/generators/app/templates/README.md +4 -2
- data/lib/adhearsion/generators/plugin/templates/lib/plugin-template.rb.tt +4 -5
- data/lib/adhearsion/linux_proc_name.rb +2 -2
- data/lib/adhearsion/version.rb +2 -2
- data/spec/adhearsion/call_controller/output_spec.rb +10 -12
- metadata +112 -141
@@ -4,38 +4,44 @@ module Adhearsion
|
|
4
4
|
class CallController
|
5
5
|
module Dial
|
6
6
|
#
|
7
|
-
# Dial
|
7
|
+
# Dial one or more third parties and join one to this call
|
8
8
|
#
|
9
|
-
# @
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
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.
|
9
|
+
# @overload dial(to[String], options = {})
|
10
|
+
# @param [String] to The target URI to dial.
|
11
|
+
# You must specify a properly formatted string that your VoIP platform understands.
|
12
|
+
# eg. sip:foo@bar.com, tel:+14044754840, or SIP/foo/1234
|
13
|
+
# @param [Hash] options see below
|
19
14
|
#
|
20
|
-
# @
|
15
|
+
# @overload dial(to[Array], options = {})
|
16
|
+
# @param [Array<String>] to Target URIs to dial.
|
17
|
+
# Each will be called with the same options simultaneously.
|
18
|
+
# The first call answered is joined, the others are hung up.
|
19
|
+
# @param [Hash] options see below
|
21
20
|
#
|
22
|
-
#
|
23
|
-
#
|
21
|
+
# @overload dial(to[Hash], options = {})
|
22
|
+
# @param [Hash<String => Hash>] to Target URIs to dial, mapped to their per-target options overrides.
|
23
|
+
# Each will be called with the same options simultaneously.
|
24
|
+
# The first call answered is joined, the others are hung up.
|
25
|
+
# Each calls options are deep-merged with the global options hash.
|
26
|
+
# @param [Hash] options see below
|
24
27
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
+
# @option options [String] :from the caller id to be used when the call is placed. It is advised you properly adhere to the
|
29
|
+
# policy of VoIP termination providers with respect to caller id values.
|
30
|
+
#
|
31
|
+
# @option options [Numeric] :for this option can be thought of best as a timeout.
|
32
|
+
# i.e. timeout after :for if no one answers the call
|
28
33
|
#
|
29
34
|
# @example Make a call to the PSTN using my SIP provider for VoIP termination
|
30
35
|
# dial "SIP/19095551001@my.sip.voip.terminator.us"
|
31
36
|
#
|
32
|
-
# @example Make 3
|
33
|
-
# for this call specified by the variable my_callerid
|
37
|
+
# @example Make 3 simulataneous calls to the SIP extensions, try for 15 seconds and use the callerid for this call specified by the variable my_callerid
|
34
38
|
# dial %w{SIP/jay-desk-650 SIP/jay-desk-601 SIP/jay-desk-601-2}, :for => 15.seconds, :from => my_callerid
|
35
39
|
#
|
36
40
|
# @example Make a call using the IAX provider to the PSTN
|
37
41
|
# dial "IAX2/my.id@voipjet/19095551234", :from => "John Doe <9095551234>"
|
38
42
|
#
|
43
|
+
# @return [DialStatus] the status of the dial operation
|
44
|
+
#
|
39
45
|
def dial(to, options = {}, latch = nil)
|
40
46
|
targets = to.respond_to?(:has_key?) ? to : Array(to)
|
41
47
|
|
@@ -111,29 +117,38 @@ module Adhearsion
|
|
111
117
|
end
|
112
118
|
|
113
119
|
class DialStatus
|
120
|
+
# The collection of calls created during the dial operation
|
114
121
|
attr_accessor :calls
|
115
122
|
|
123
|
+
# @private
|
116
124
|
def initialize
|
117
125
|
@result = nil
|
118
126
|
end
|
119
127
|
|
128
|
+
#
|
129
|
+
# The result of the dial operation.
|
130
|
+
#
|
131
|
+
# @return [Symbol] :no_answer, :answer, :timeout, :error
|
120
132
|
def result
|
121
133
|
@result || :no_answer
|
122
134
|
end
|
123
135
|
|
136
|
+
# @private
|
124
137
|
def answer!
|
125
138
|
@result = :answer
|
126
139
|
end
|
127
140
|
|
141
|
+
# @private
|
128
142
|
def timeout!
|
129
143
|
@result ||= :timeout
|
130
144
|
end
|
131
145
|
|
146
|
+
# @private
|
132
147
|
def error!
|
133
148
|
@result ||= :error
|
134
149
|
end
|
135
150
|
end
|
136
151
|
|
137
|
-
end
|
152
|
+
end
|
138
153
|
end
|
139
154
|
end
|
@@ -10,6 +10,7 @@ module Adhearsion
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
+
#
|
13
14
|
# Prompts for input via DTMF, handling playback of prompts,
|
14
15
|
# timeouts, digit limits and terminator digits.
|
15
16
|
#
|
@@ -23,7 +24,7 @@ module Adhearsion
|
|
23
24
|
# :timeout, :terminator and :limit options may then be specified.
|
24
25
|
# A block may be passed which is invoked on each digit being collected. If it returns true, the collection is terminated.
|
25
26
|
#
|
26
|
-
# @param [Object] A list of outputs to play, as accepted by #play
|
27
|
+
# @param [Object, Array<Object>] args A list of outputs to play, as accepted by #play
|
27
28
|
# @param [Hash] options Options to use for the menu
|
28
29
|
# @option options [Boolean] :interruptible If the prompt should be interruptible or not. Defaults to true
|
29
30
|
# @option options [Integer] :limit Digit limit (causes collection to cease after a specified number of digits have been collected)
|
@@ -32,8 +33,8 @@ module Adhearsion
|
|
32
33
|
#
|
33
34
|
# @return [Result] a result object from which the #response and #status may be established
|
34
35
|
#
|
35
|
-
# @see play
|
36
|
-
# @see pass
|
36
|
+
# @see Output#play
|
37
|
+
# @see CallController#pass
|
37
38
|
#
|
38
39
|
def ask(*args, &block)
|
39
40
|
options = args.last.kind_of?(Hash) ? args.pop : {}
|
@@ -51,8 +52,8 @@ module Adhearsion
|
|
51
52
|
if result_of_menu.is_a?(MenuDSL::Menu::MenuGetAnotherDigit)
|
52
53
|
next_digit = play_sound_files_for_menu menu_instance, sound_files
|
53
54
|
menu_instance << next_digit if next_digit
|
54
|
-
end
|
55
|
-
end
|
55
|
+
end
|
56
|
+
end
|
56
57
|
|
57
58
|
Result.new.tap do |result|
|
58
59
|
result.response = menu_instance.result
|
@@ -103,7 +104,7 @@ module Adhearsion
|
|
103
104
|
# Execution of the current context resumes after #ask finishes. If you wish to jump to an entirely different controller, use #pass.
|
104
105
|
# Menu will return :failed if failure was reached, or :done if a match was executed.
|
105
106
|
#
|
106
|
-
# @param [Object] A list of outputs to play, as accepted by #play
|
107
|
+
# @param [Object] args A list of outputs to play, as accepted by #play
|
107
108
|
# @param [Hash] options Options to use for the menu
|
108
109
|
# @option options [Integer] :tries Number of tries allowed before failure
|
109
110
|
# @option options [Integer] :timeout Timeout in seconds before the first and between each input digit
|
@@ -111,8 +112,8 @@ module Adhearsion
|
|
111
112
|
#
|
112
113
|
# @return [Result] a result object from which the #response and #status may be established
|
113
114
|
#
|
114
|
-
# @see play
|
115
|
-
# @see pass
|
115
|
+
# @see Output#play
|
116
|
+
# @see CallController#pass
|
116
117
|
#
|
117
118
|
def menu(*args, &block)
|
118
119
|
options = args.last.kind_of?(Hash) ? args.pop : {}
|
@@ -158,8 +159,8 @@ module Adhearsion
|
|
158
159
|
logger.debug "Menu received valid input (#{result_of_menu.new_extension}). Calling the matching hook."
|
159
160
|
jump_to result_of_menu.match_object, :extension => result_of_menu.new_extension
|
160
161
|
throw :finish
|
161
|
-
end
|
162
|
-
end
|
162
|
+
end
|
163
|
+
end
|
163
164
|
end
|
164
165
|
|
165
166
|
Result.new.tap do |result|
|
@@ -169,7 +170,8 @@ module Adhearsion
|
|
169
170
|
end
|
170
171
|
end
|
171
172
|
|
172
|
-
|
173
|
+
# @private
|
174
|
+
def play_sound_files_for_menu(menu_instance, sound_files)
|
173
175
|
digit = nil
|
174
176
|
if sound_files.any? && menu_instance.digit_buffer_empty?
|
175
177
|
if menu_instance.interruptible
|
@@ -185,9 +187,11 @@ module Adhearsion
|
|
185
187
|
# Waits for a single digit and returns it, or returns nil if nothing was pressed
|
186
188
|
#
|
187
189
|
# @param [Integer] the timeout to wait before returning, in seconds. nil or -1 mean no timeout.
|
188
|
-
# @return [String
|
190
|
+
# @return [String, nil] the pressed key, or nil if timeout was reached.
|
191
|
+
#
|
192
|
+
# @private
|
189
193
|
#
|
190
|
-
def wait_for_digit(timeout = 1)
|
194
|
+
def wait_for_digit(timeout = 1)
|
191
195
|
timeout = nil if timeout == -1
|
192
196
|
timeout *= 1_000 if timeout
|
193
197
|
input_component = execute_component_and_await_completion ::Punchblock::Component::Input.new :mode => :dtmf,
|
@@ -202,7 +206,8 @@ module Adhearsion
|
|
202
206
|
parse_single_dtmf result
|
203
207
|
end
|
204
208
|
|
205
|
-
|
209
|
+
# @private
|
210
|
+
def jump_to(match_object, overrides = nil)
|
206
211
|
if match_object.block
|
207
212
|
instance_exec overrides[:extension], &match_object.block
|
208
213
|
else
|
@@ -210,6 +215,6 @@ module Adhearsion
|
|
210
215
|
end
|
211
216
|
end
|
212
217
|
|
213
|
-
end
|
218
|
+
end
|
214
219
|
end
|
215
220
|
end
|
@@ -5,8 +5,14 @@ module Adhearsion
|
|
5
5
|
module Output
|
6
6
|
PlaybackError = Class.new Adhearsion::Error # Represents failure to play audio, such as when the sound file cannot be found
|
7
7
|
|
8
|
+
#
|
9
|
+
# Speak output using text-to-speech (TTS)
|
10
|
+
#
|
11
|
+
# @param [String, #to_s] text The text to be rendered
|
12
|
+
# @param [Hash] options A set of options for output
|
13
|
+
#
|
8
14
|
def say(text, options = {})
|
9
|
-
play_ssml(text, options) || output(
|
15
|
+
play_ssml(text, options) || output(ssml_for_text(text.to_s), options)
|
10
16
|
end
|
11
17
|
alias :speak :say
|
12
18
|
|
@@ -54,6 +60,8 @@ module Adhearsion
|
|
54
60
|
# Plays the specified input arguments, raising an exception if any can't be played.
|
55
61
|
# @see play
|
56
62
|
#
|
63
|
+
# @private
|
64
|
+
#
|
57
65
|
def play!(*arguments)
|
58
66
|
play(*arguments) or raise PlaybackError, "One of the passed outputs is invalid"
|
59
67
|
end
|
@@ -62,25 +70,22 @@ module Adhearsion
|
|
62
70
|
# Plays the given Date, Time, or Integer (seconds since epoch)
|
63
71
|
# using the given timezone and format.
|
64
72
|
#
|
65
|
-
# @param [Date
|
66
|
-
# @param [Hash] Additional options to specify how exactly to say time specified.
|
67
|
-
#
|
68
|
-
# +:format+ - This format is used only to disambiguate times that could be interpreted in different ways.
|
73
|
+
# @param [Date, Time, DateTime] time Time to be said.
|
74
|
+
# @param [Hash] options Additional options to specify how exactly to say time specified.
|
75
|
+
# @option options [String] :format This format is used only to disambiguate times that could be interpreted in different ways.
|
69
76
|
# For example, 01/06/2011 could mean either the 1st of June or the 6th of January.
|
70
77
|
# Please refer to the SSML specification.
|
71
78
|
# @see http://www.w3.org/TR/ssml-sayas/#S3.1
|
72
|
-
#
|
79
|
+
# @option options [String] :strftime This format is what defines the string that is sent to the Speech Synthesis Engine.
|
73
80
|
# It uses Time::strftime symbols.
|
74
81
|
#
|
75
82
|
# @return [Boolean] true if successful, false if the given argument could not be played.
|
76
83
|
#
|
77
|
-
def play_time(
|
78
|
-
|
79
|
-
return false unless [Date, Time, DateTime].include? argument.class
|
84
|
+
def play_time(time, options = {})
|
85
|
+
return false unless [Date, Time, DateTime].include? time.class
|
80
86
|
|
81
|
-
options ||= {}
|
82
87
|
return false unless options.is_a? Hash
|
83
|
-
play_ssml ssml_for_time(
|
88
|
+
play_ssml ssml_for_time(time, options)
|
84
89
|
end
|
85
90
|
|
86
91
|
#
|
@@ -88,14 +93,13 @@ module Adhearsion
|
|
88
93
|
# When playing numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
|
89
94
|
# is pronounced as "one hundred" instead of "one zero zero".
|
90
95
|
#
|
91
|
-
# @param [Numeric
|
96
|
+
# @param [Numeric, String] Numeric or String containing a valid Numeric, like "321".
|
92
97
|
#
|
93
98
|
# @return [Boolean] true if successful, false if the given argument could not be played.
|
94
99
|
#
|
95
|
-
def play_numeric(
|
96
|
-
|
97
|
-
|
98
|
-
play_ssml ssml_for_numeric(argument, options)
|
100
|
+
def play_numeric(number, options = nil)
|
101
|
+
if number.kind_of?(Numeric) || number =~ /^\d+$/
|
102
|
+
play_ssml ssml_for_numeric(number, options)
|
99
103
|
end
|
100
104
|
end
|
101
105
|
|
@@ -104,30 +108,26 @@ module Adhearsion
|
|
104
108
|
# SSML supports http:// paths and full disk paths.
|
105
109
|
# The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory.
|
106
110
|
#
|
107
|
-
# @param [String] http:// URL or full disk path to the sound file
|
108
|
-
# @param [Hash] Additional options to specify how exactly to say time specified.
|
109
|
-
#
|
111
|
+
# @param [String] file http:// URL or full disk path to the sound file
|
112
|
+
# @param [Hash] options Additional options to specify how exactly to say time specified.
|
113
|
+
# @option options [String] :fallback The text to play if the file is not available
|
110
114
|
#
|
111
115
|
# @return [Boolean] true on correct play of the file, false on file missing or not playable
|
112
116
|
#
|
113
|
-
def play_audio(
|
114
|
-
|
115
|
-
play_ssml ssml_for_audio(argument, options)
|
117
|
+
def play_audio(file, options = nil)
|
118
|
+
play_ssml ssml_for_audio(file, options)
|
116
119
|
end
|
117
120
|
|
118
|
-
|
121
|
+
# @private
|
122
|
+
def play_ssml(ssml, options = {})
|
119
123
|
if [RubySpeech::SSML::Speak, Nokogiri::XML::Document].include? ssml.class
|
120
|
-
output
|
124
|
+
output ssml.to_s, options
|
121
125
|
end
|
122
126
|
end
|
123
127
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
end
|
128
|
-
|
129
|
-
def output!(type, content, options = {}) # :nodoc:
|
130
|
-
options.merge! type => content
|
128
|
+
# @private
|
129
|
+
def output(content, options = {})
|
130
|
+
options.merge! :ssml => content
|
131
131
|
execute_component_and_await_completion ::Punchblock::Component::Output.new(options)
|
132
132
|
end
|
133
133
|
|
@@ -135,6 +135,8 @@ module Adhearsion
|
|
135
135
|
# Same as interruptible_play, but throws an error if unable to play the output
|
136
136
|
# @see interruptible_play
|
137
137
|
#
|
138
|
+
# @private
|
139
|
+
#
|
138
140
|
def interruptible_play!(*outputs)
|
139
141
|
result = nil
|
140
142
|
outputs.each do |output|
|
@@ -155,10 +157,10 @@ module Adhearsion
|
|
155
157
|
# input = interruptible_play ssml
|
156
158
|
# play input unless input.nil?
|
157
159
|
#
|
158
|
-
# @param [String
|
160
|
+
# @param [String, Numeric, Date, Time, RubySpeech::SSML::Speak, Array, Hash] The argument to play to the user, or an array of arguments.
|
159
161
|
# @param [Hash] Additional options.
|
160
162
|
#
|
161
|
-
# @return [String
|
163
|
+
# @return [String, nil] The single DTMF character entered by the user, or nil if nothing was entered
|
162
164
|
#
|
163
165
|
def interruptible_play(*outputs)
|
164
166
|
result = nil
|
@@ -175,7 +177,8 @@ module Adhearsion
|
|
175
177
|
result
|
176
178
|
end
|
177
179
|
|
178
|
-
|
180
|
+
# @private
|
181
|
+
def detect_type(output)
|
179
182
|
result = nil
|
180
183
|
result = :time if [Date, Time, DateTime].include? output.class
|
181
184
|
result = :numeric if output.kind_of?(Numeric) || output =~ /^\d+$/
|
@@ -183,7 +186,8 @@ module Adhearsion
|
|
183
186
|
result ||= :text
|
184
187
|
end
|
185
188
|
|
186
|
-
|
189
|
+
# @private
|
190
|
+
def play_ssml_for(*args)
|
187
191
|
play_ssml ssml_for(args)
|
188
192
|
end
|
189
193
|
|
@@ -191,10 +195,12 @@ module Adhearsion
|
|
191
195
|
# Generates SSML for the argument and options passed, using automatic detection
|
192
196
|
# Directly returns the argument if it is already an SSML document
|
193
197
|
#
|
194
|
-
# @param [String
|
198
|
+
# @param [String, Hash, RubySpeech::SSML::Speak] the argument with options as accepted by the play_ methods, or an SSML document
|
195
199
|
# @return [RubySpeech::SSML::Speak] an SSML document
|
196
200
|
#
|
197
|
-
|
201
|
+
# @private
|
202
|
+
#
|
203
|
+
def ssml_for(*args)
|
198
204
|
return args[0] if args.size == 1 && args[0].is_a?(RubySpeech::SSML::Speak)
|
199
205
|
argument, options = args.flatten
|
200
206
|
options ||= {}
|
@@ -202,11 +208,13 @@ module Adhearsion
|
|
202
208
|
send "ssml_for_#{type}", argument, options
|
203
209
|
end
|
204
210
|
|
205
|
-
|
211
|
+
# @private
|
212
|
+
def ssml_for_text(argument, options = {})
|
206
213
|
RubySpeech::SSML.draw { argument }
|
207
214
|
end
|
208
215
|
|
209
|
-
|
216
|
+
# @private
|
217
|
+
def ssml_for_time(argument, options = {})
|
210
218
|
interpretation = case argument
|
211
219
|
when Date then 'date'
|
212
220
|
when Time then 'time'
|
@@ -222,13 +230,15 @@ module Adhearsion
|
|
222
230
|
end
|
223
231
|
end
|
224
232
|
|
225
|
-
|
233
|
+
# @private
|
234
|
+
def ssml_for_numeric(argument, options = {})
|
226
235
|
RubySpeech::SSML.draw do
|
227
236
|
say_as(:interpret_as => 'cardinal') { argument.to_s }
|
228
237
|
end
|
229
238
|
end
|
230
239
|
|
231
|
-
|
240
|
+
# @private
|
241
|
+
def ssml_for_audio(argument, options = {})
|
232
242
|
fallback = (options || {}).delete :fallback
|
233
243
|
RubySpeech::SSML.draw do
|
234
244
|
audio(:src => argument) { fallback }
|
@@ -241,7 +251,8 @@ module Adhearsion
|
|
241
251
|
#
|
242
252
|
# @param [Object] String or Hash specifying output and options
|
243
253
|
# @param [String] String with the digits that are allowed to interrupt output
|
244
|
-
#
|
254
|
+
#
|
255
|
+
# @return [String, nil] The pressed digit, or nil if nothing was pressed
|
245
256
|
#
|
246
257
|
def stream_file(argument, digits = '0123456789#*')
|
247
258
|
result = nil
|
@@ -9,7 +9,7 @@ module Adhearsion
|
|
9
9
|
# Start a recording
|
10
10
|
#
|
11
11
|
# @param [Hash] options
|
12
|
-
# @param [Block]
|
12
|
+
# @param [Block] block to process result of the record method
|
13
13
|
# @option options [Boolean, Optional] :async Execute asynchronously. Defaults to false
|
14
14
|
# @option options [Boolean, Optional] :start_beep Indicates whether subsequent record will be preceded with a beep. Default is true.
|
15
15
|
# @option options [Boolean, Optional] :start_paused Whether subsequent record will start in PAUSE mode. Default is false.
|
@@ -19,8 +19,8 @@ module Adhearsion
|
|
19
19
|
# @option options [String, Optional] :initial_timeout Controls how long (milliseconds) the recognizer should wait after the end of the prompt for the caller to speak before sending a Recorder event.
|
20
20
|
# @option options [String, Optional] :final_timeout Controls the length (milliseconds) of a period of silence after callers have spoken to conclude they finished.
|
21
21
|
#
|
22
|
-
# @return
|
23
|
-
|
22
|
+
# @return Punchblock::Component::Record::Recording
|
23
|
+
#
|
24
24
|
def record(options = {})
|
25
25
|
async = options.delete :async
|
26
26
|
|