adhearsion 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.rspec +3 -0
- data/CHANGELOG +12 -0
- data/README.markdown +1 -10
- data/Rakefile +6 -28
- data/adhearsion.gemspec +21 -38
- data/app_generators/ahn/ahn_generator.rb +3 -4
- data/app_generators/ahn/templates/config/environment.rb +4 -0
- data/app_generators/ahn/templates/config/startup.rb +11 -5
- data/app_generators/ahn/templates/script/ahn +8 -0
- data/lib/adhearsion/cli.rb +5 -298
- data/lib/adhearsion/commands.rb +308 -0
- data/lib/adhearsion/events_support.rb +0 -20
- data/lib/adhearsion/initializer/asterisk.rb +0 -1
- data/lib/adhearsion/initializer/configuration.rb +4 -1
- data/lib/adhearsion/logging.rb +18 -3
- data/lib/adhearsion/script_ahn_loader.rb +30 -0
- data/lib/adhearsion/tasks/testing.rb +39 -8
- data/lib/adhearsion/version.rb +2 -2
- data/lib/adhearsion/voip/asterisk/agi_server.rb +15 -8
- data/lib/adhearsion/voip/asterisk/commands.rb +340 -86
- data/lib/adhearsion/voip/asterisk/manager_interface.rb +0 -2
- data/lib/adhearsion/voip/call.rb +11 -3
- data/lib/adhearsion/voip/commands.rb +5 -1
- data/lib/adhearsion/voip/dial_plan.rb +4 -0
- data/lib/theatre/invocation.rb +3 -0
- data/spec/{ahn_command_spec.rb → adhearsion/cli_spec.rb} +11 -0
- data/spec/{component_manager_spec.rb → adhearsion/component_manager_spec.rb} +0 -0
- data/spec/{constants_spec.rb → adhearsion/constants_spec.rb} +0 -0
- data/spec/{drb_spec.rb → adhearsion/drb_spec.rb} +0 -0
- data/spec/{fixtures → adhearsion/fixtures}/dialplan.rb +0 -0
- data/spec/{foundation → adhearsion/foundation}/event_socket_spec.rb +0 -0
- data/spec/{host_definitions_spec.rb → adhearsion/host_definitions_spec.rb} +0 -0
- data/spec/{initializer → adhearsion/initializer}/configuration_spec.rb +21 -0
- data/spec/{initializer → adhearsion/initializer}/loading_spec.rb +0 -0
- data/spec/{initializer → adhearsion/initializer}/paths_spec.rb +0 -0
- data/spec/{initialization_spec.rb → adhearsion/initializer_spec.rb} +0 -0
- data/spec/{logging_spec.rb → adhearsion/logging_spec.rb} +6 -0
- data/spec/{relationship_properties_spec.rb → adhearsion/relationship_properties_spec.rb} +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/agi_server_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/ami/ami_spec.rb +1 -0
- data/spec/{voip → adhearsion/voip}/asterisk/ami/lexer/ami_fixtures.yml +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/ami/lexer/lexer_story +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/ami/lexer/lexer_story.rb +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/ami/lexer/story_helper.rb +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/commands_spec.rb +549 -47
- data/spec/{voip → adhearsion/voip}/asterisk/config_file_generators/agents_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/config_file_generators/queues_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/config_file_generators/voicemail_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/config_manager_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/menu_command/calculated_match_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/asterisk/menu_command/matchers_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/call_routing_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/dialplan_manager_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/dsl/dialing_dsl_spec.rb +1 -1
- data/spec/{voip → adhearsion/voip}/dsl/dispatcher_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/dsl/dispatcher_spec_helper.rb +0 -0
- data/spec/{voip → adhearsion/voip}/dsl/parser_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/freeswitch/basic_connection_manager_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/freeswitch/inbound_connection_manager_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/freeswitch/oes_server_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/numerical_string_spec.rb +0 -0
- data/spec/{voip → adhearsion/voip}/phone_number_spec.rb +0 -0
- data/spec/spec_helper.rb +36 -89
- data/spec/support/initializer_stubs.rb +47 -0
- data/spec/support/the_following_code.rb +3 -0
- data/{theatre-spec → spec/theatre}/dsl_examples/simple_before_call.rb +0 -0
- data/{theatre-spec → spec/theatre}/dsl_spec.rb +26 -0
- data/{theatre-spec → spec/theatre}/invocation_spec.rb +6 -10
- data/{theatre-spec → spec/theatre}/namespace_spec.rb +0 -0
- data/{theatre-spec → spec/theatre}/spec_helper_spec.rb +0 -0
- data/{theatre-spec → spec/theatre}/theatre_class_spec.rb +2 -5
- metadata +254 -271
- data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.rb +0 -104
- data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.yml +0 -2
- data/lib/adhearsion/voip/asterisk/super_manager.rb +0 -19
- data/spec/silence.rb +0 -10
- data/spec/voip/asterisk/ami/old_tests.rb +0 -204
- data/spec/voip/asterisk/ami/super_manager/super_manager_story +0 -25
- data/spec/voip/asterisk/ami/super_manager/super_manager_story.rb +0 -15
- data/spec/voip/asterisk/ami/super_manager/super_manager_story_helper.rb +0 -5
- data/theatre-spec/dsl_examples/dynamic_stomp.rb +0 -7
- data/theatre-spec/spec_helper.rb +0 -37
data/lib/adhearsion/version.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Adhearsion #:nodoc:
|
2
2
|
module VERSION #:nodoc:
|
3
3
|
MAJOR = 1 unless defined? MAJOR
|
4
|
-
MINOR =
|
5
|
-
TINY =
|
4
|
+
MINOR = 2 unless defined? MINOR
|
5
|
+
TINY = 0 unless defined? TINY
|
6
6
|
|
7
7
|
STRING = [MAJOR, MINOR, TINY].join('.') unless defined? STRING
|
8
8
|
end
|
@@ -25,17 +25,17 @@ module Adhearsion
|
|
25
25
|
end
|
26
26
|
|
27
27
|
Events.trigger_immediately([:asterisk, :before_call], call)
|
28
|
-
|
28
|
+
ahn_log.agi.debug "Handling call with variables #{call.variables.inspect}"
|
29
29
|
|
30
|
-
|
30
|
+
return DialPlan::ConfirmationManager.handle(call) if DialPlan::ConfirmationManager.confirmation_call?(call)
|
31
31
|
|
32
|
-
|
32
|
+
# This is what happens 99.9% of the time.
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
DialPlan::Manager.handle call
|
35
|
+
rescue Hangup
|
36
|
+
ahn_log.agi "HANGUP event for call with uniqueid #{call.variables[:uniqueid].inspect} and channel #{call.variables[:channel].inspect}"
|
37
37
|
Events.trigger_immediately([:asterisk, :after_call], call)
|
38
|
-
|
38
|
+
call.hangup!
|
39
39
|
rescue DialPlan::Manager::NoContextError => e
|
40
40
|
ahn_log.agi e.message
|
41
41
|
call.hangup!
|
@@ -58,13 +58,20 @@ module Adhearsion
|
|
58
58
|
rescue UselessCallException
|
59
59
|
ahn_log.agi "Ignoring meta-AGI request"
|
60
60
|
call.hangup!
|
61
|
-
|
61
|
+
# TBD: (may have more hooks than what Jay has defined in hooks.rb)
|
62
62
|
rescue SyntaxError, StandardError => e
|
63
63
|
Events.trigger(['exception'], e)
|
64
64
|
ensure
|
65
65
|
Adhearsion.remove_inactive_call call rescue nil
|
66
66
|
end
|
67
67
|
|
68
|
+
def log(msg)
|
69
|
+
ahn_log.agi msg
|
70
|
+
end
|
71
|
+
|
72
|
+
def error(detail)
|
73
|
+
ahn_log.agi.error detail.backtrace.join("\n")
|
74
|
+
end
|
68
75
|
end
|
69
76
|
|
70
77
|
DEFAULT_OPTIONS = { :server_class => RubyServer, :port => 4573, :host => "0.0.0.0" } unless defined? DEFAULT_OPTIONS
|
@@ -1,12 +1,15 @@
|
|
1
|
-
|
2
1
|
require 'adhearsion/voip/menu_state_machine/menu_class'
|
2
|
+
require 'json'
|
3
3
|
|
4
4
|
module Adhearsion
|
5
5
|
module VoIP
|
6
6
|
module Asterisk
|
7
|
+
class AGIProtocolError < StandardError; end
|
8
|
+
|
7
9
|
module Commands
|
8
10
|
|
9
11
|
RESPONSE_PREFIX = "200 result=" unless defined? RESPONSE_PREFIX
|
12
|
+
AGI_SUCCESSFUL_RESPONSE = RESPONSE_PREFIX + "1"
|
10
13
|
|
11
14
|
# These are the status messages that asterisk will issue after a dial command is executed.
|
12
15
|
#
|
@@ -47,7 +50,7 @@ module Adhearsion
|
|
47
50
|
# Utility method to write to pbx.
|
48
51
|
# @param [String] message raw message
|
49
52
|
def write(message)
|
50
|
-
to_pbx.print
|
53
|
+
to_pbx.print message + "\n"
|
51
54
|
end
|
52
55
|
|
53
56
|
# Utility method to read from pbx. Hangup if nil.
|
@@ -96,6 +99,7 @@ module Adhearsion
|
|
96
99
|
#
|
97
100
|
# @see http://www.voip-info.org/wiki/view/Asterisk+FastAGI More information about FAGI
|
98
101
|
def raw_response(message = nil)
|
102
|
+
message.squish!
|
99
103
|
@call.with_command_lock do
|
100
104
|
raise ArgumentError.new("illegal NUL in message #{message.inspect}") if message =~ /\0/
|
101
105
|
ahn_log.agi.debug ">>> #{message}"
|
@@ -105,18 +109,40 @@ module Adhearsion
|
|
105
109
|
end
|
106
110
|
|
107
111
|
def response(command, *arguments)
|
108
|
-
# Arguments surrounded by quotes; quotes backslash-escaped.
|
109
|
-
# See parse_args in asterisk/res/res_agi.c (Asterisk 1.4.21.1)
|
110
|
-
quote_arg = lambda { |arg|
|
111
|
-
'"' + arg.gsub(/["\\]/) { |m| "\\#{m}" } + '"'
|
112
|
-
}
|
113
112
|
if arguments.empty?
|
114
113
|
raw_response("#{command}")
|
115
114
|
else
|
116
|
-
raw_response("#{command} " + arguments.map{ |arg| quote_arg
|
115
|
+
raw_response("#{command} " + arguments.map{ |arg| quote_arg(arg) }.join(' '))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Arguments surrounded by quotes; quotes backslash-escaped.
|
120
|
+
# See parse_args in asterisk/res/res_agi.c (Asterisk 1.4.21.1)
|
121
|
+
def quote_arg(arg)
|
122
|
+
'"' + arg.to_s.gsub(/["\\]/) { |m| "\\#{m}" } + '"'
|
123
|
+
end
|
124
|
+
|
125
|
+
# Parses a response in the form of "200 result=some_value"
|
126
|
+
def inline_return_value(result)
|
127
|
+
return nil unless result
|
128
|
+
case result.chomp
|
129
|
+
when "200 result=0" then nil
|
130
|
+
when /^200 result=(.*)$/ then $LAST_PAREN_MATCH
|
131
|
+
else raise AGIProtocolError, "Invalid AGI response: #{result}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Parses a response in the form of "200 result=0 (some_value)"
|
136
|
+
def inline_result_with_return_value(result)
|
137
|
+
return nil unless result
|
138
|
+
case result.chomp
|
139
|
+
when "200 result=0" then nil
|
140
|
+
when /^#{AGI_SUCCESSFUL_RESPONSE} \((.*)\)$/ then $LAST_PAREN_MATCH
|
141
|
+
else raise AGIProtocolError, "Invalid AGI response: #{result}"
|
117
142
|
end
|
118
143
|
end
|
119
144
|
|
145
|
+
|
120
146
|
# This must be called first before any other commands can be issued.
|
121
147
|
# In typical Adhearsion applications this is called by default as soon as a call is
|
122
148
|
# transfered to a valid context in dialplan.rb.
|
@@ -144,8 +170,9 @@ module Adhearsion
|
|
144
170
|
#
|
145
171
|
# @see http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands Asterisk Dialplan Commands
|
146
172
|
def execute(application, *arguments)
|
147
|
-
|
148
|
-
|
173
|
+
command = "EXEC #{application}"
|
174
|
+
arguments = arguments.map { |arg| quote_arg(arg) }.join(AHN_CONFIG.asterisk.argument_delimiter)
|
175
|
+
result = raw_response("#{command} #{arguments}")
|
149
176
|
return false if error?(result)
|
150
177
|
result
|
151
178
|
end
|
@@ -163,7 +190,7 @@ module Adhearsion
|
|
163
190
|
#
|
164
191
|
# @see http://www.voip-info.org/wiki/view/verbose
|
165
192
|
def verbose(message, level = nil)
|
166
|
-
result =
|
193
|
+
result = response('VERBOSE', message, level)
|
167
194
|
return false if error?(result)
|
168
195
|
result
|
169
196
|
end
|
@@ -207,7 +234,7 @@ module Adhearsion
|
|
207
234
|
arguments.flatten.each do |argument|
|
208
235
|
# result starts off as true. But if the following command ever returns false, then result
|
209
236
|
# remains false.
|
210
|
-
result &= play_numeric(argument) ||
|
237
|
+
result &= play_numeric(argument) || play_soundfile(argument)
|
211
238
|
end
|
212
239
|
end
|
213
240
|
result
|
@@ -220,18 +247,138 @@ module Adhearsion
|
|
220
247
|
def play!(*arguments)
|
221
248
|
unless play_time(arguments)
|
222
249
|
arguments.flatten.each do |argument|
|
223
|
-
play_numeric(argument) ||
|
250
|
+
play_numeric(argument) || play_soundfile!(argument)
|
224
251
|
end
|
225
252
|
end
|
226
253
|
true
|
227
254
|
end
|
228
255
|
|
256
|
+
# Attempts to play a sound prompt. If the prompt is unplayable, for
|
257
|
+
# example, if the file is not present, then attempt to speak the prompt
|
258
|
+
# using Text-To-Speech.
|
259
|
+
#
|
260
|
+
# @param [Hash] Map of prompts and fallback TTS options
|
261
|
+
# @return [true]
|
262
|
+
# @raise [ArgumentError] If prompt cannot be found and TTS text is not specified
|
263
|
+
#
|
264
|
+
# @example Play "tt-monkeys" or say "Ooh ooh eee eee eee"
|
265
|
+
# play_or_speak 'tt-monkeys' => {:text => "Ooh ooh eee eee eee"}
|
266
|
+
#
|
267
|
+
# @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 "#"
|
268
|
+
# play_or_speak 'pbx-invalid' => {:text => "I'm sorry, that is not a valid extension. Please try again", :engine => :unimrcp}
|
269
|
+
def play_or_speak(prompts)
|
270
|
+
interrupted = nil
|
271
|
+
unless interrupted
|
272
|
+
prompts.each do |filename, options|
|
273
|
+
if filename && !filename.empty?
|
274
|
+
begin
|
275
|
+
if options[:interruptible]
|
276
|
+
interrupted = interruptible_play! filename
|
277
|
+
else
|
278
|
+
play! filename
|
279
|
+
end
|
280
|
+
rescue PlaybackError
|
281
|
+
raise ArgumentError, "Must supply TTS text as fallback" unless options[:text]
|
282
|
+
interrupted = speak options.delete(:text), options
|
283
|
+
end
|
284
|
+
else
|
285
|
+
interrupted = speak options.delete(:text), options
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
interrupted
|
290
|
+
end
|
291
|
+
|
292
|
+
# Records a sound file with the given name. If no filename is specified a file named by Asterisk
|
293
|
+
# will be created and returned. Else the given filename will be returned. If a relative path is
|
294
|
+
# given, the file will be saved in the default Asterisk sound directory, /var/lib/spool/asterisk
|
295
|
+
# by default.
|
296
|
+
#
|
297
|
+
# @param [string] file name to record to. Full path information is optional. If you want to change the
|
298
|
+
# format of the file you will want to add a .<valid extention> to the end of the file name specifying the
|
299
|
+
# filetype you want to record in. Alternately you can pass it is as :format in the options
|
300
|
+
#
|
301
|
+
# @param [hash] options
|
302
|
+
#
|
303
|
+
# +:silence+ - silence in seconds
|
304
|
+
#
|
305
|
+
# +:maxduration+ - maximum duration in seconds
|
306
|
+
#
|
307
|
+
# +:escapedigits+ - digits to be used to excape from recording
|
308
|
+
#
|
309
|
+
# +:beep+ - soundfile to use as a beep before recording. if not specifed defaults to system generated beep, set to nil for no beep.
|
310
|
+
#
|
311
|
+
# +:format+ - the format of the file to be recorded
|
312
|
+
#
|
313
|
+
# Silence and maxduration is specified in seconds.
|
314
|
+
#
|
315
|
+
# @return [String] The filename of the recorded file.
|
316
|
+
#
|
317
|
+
# @example Asterisk generated filename
|
318
|
+
# filename = record
|
319
|
+
# @example Specified filename
|
320
|
+
# record '/path/to/my-file.gsm'
|
321
|
+
# @example All options specified
|
322
|
+
# record 'my-file.gsm', :silence => 5, :maxduration => 120
|
323
|
+
#
|
324
|
+
# @deprecated please use {#record_to_file} instead
|
325
|
+
def record(*args)
|
326
|
+
options = args.last.kind_of?(Hash) ? args.last : {}
|
327
|
+
filename = args.first && !args.first.kind_of?(Hash) ? String.new(args.first) : "/tmp/recording_%d"
|
328
|
+
if filename.index("%d")
|
329
|
+
if @call.variables.has_key?(:recording_counter)
|
330
|
+
@call.variables[:recording_counter] += 1
|
331
|
+
else
|
332
|
+
@call.variables[:recording_counter] = 0
|
333
|
+
end
|
334
|
+
filename = filename % @call.variables[:recording_counter]
|
335
|
+
@call.variables[:recording_counter] -= 1
|
336
|
+
end
|
337
|
+
|
338
|
+
if (!options.has_key?(:format))
|
339
|
+
format = filename.slice!(/\.[^\.]+$/)
|
340
|
+
if (format.nil?)
|
341
|
+
ahn_log.agi.warn "Format not specified and not detected. Defaulting to \"gsm\""
|
342
|
+
format = "gsm"
|
343
|
+
end
|
344
|
+
format.sub!(/^\./, "")
|
345
|
+
else
|
346
|
+
format = options[:format]
|
347
|
+
end
|
348
|
+
record_to_file(*args)
|
349
|
+
filename + "." + format
|
350
|
+
end
|
351
|
+
|
229
352
|
# Records a sound file with the given name. If no filename is specified a file named by Asterisk
|
230
353
|
# will be created and returned. Else the given filename will be returned. If a relative path is
|
231
354
|
# given, the file will be saved in the default Asterisk sound directory, /var/lib/spool/asterisk
|
232
355
|
# by default.
|
233
356
|
#
|
357
|
+
# @param [string] file name to record to. Full path information is optional. If you want to change the
|
358
|
+
# format of the file you will want to add a .<valid extention> to the end of the file name specifying the
|
359
|
+
# filetype you want to record in. If you don't specify a valid extension it will default to gsm and a
|
360
|
+
# .gsm will be added to the file. If you don't specify a filename it will write one in /tmp/recording_%d
|
361
|
+
# with %d being a counter that increments from 0 onward for the particular call you are making.
|
362
|
+
#
|
363
|
+
# @param [hash] options
|
364
|
+
#
|
365
|
+
# +:silence+ - silence in seconds
|
366
|
+
#
|
367
|
+
# +:maxduration+ - maximum duration in seconds
|
368
|
+
#
|
369
|
+
# +:escapedigits+ - digits to be used to excape from recording
|
370
|
+
#
|
371
|
+
# +:beep+ - soundfile to use as a beep before recording. if not specifed defaults to system generated beep, set to nil for no beep.
|
372
|
+
#
|
373
|
+
# +: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.
|
374
|
+
#
|
234
375
|
# Silence and maxduration is specified in seconds.
|
376
|
+
#
|
377
|
+
# @return [Symbol] One of the follwing..... :hangup, :write_error, :success_dtmf, :success_timeout
|
378
|
+
#
|
379
|
+
# A sound file will be recorded to the specifed file unless a :write_error is returned. A :success_dtmf is
|
380
|
+
# for when a call was ended with a DTMF tone. A :success_timeout is returned when a call times out due to
|
381
|
+
# a silence longer than the specified silence or if the recording reaches the maxduration.
|
235
382
|
#
|
236
383
|
# @example Asterisk generated filename
|
237
384
|
# filename = record
|
@@ -240,7 +387,26 @@ module Adhearsion
|
|
240
387
|
# @example All options specified
|
241
388
|
# record 'my-file.gsm', :silence => 5, :maxduration => 120
|
242
389
|
#
|
243
|
-
def
|
390
|
+
def record_to_file(*args)
|
391
|
+
base_record_to_file(*args).last
|
392
|
+
end
|
393
|
+
|
394
|
+
# This works the same record_to_file except is throws an exception if a playback or write error occurs.
|
395
|
+
#
|
396
|
+
def record_to_file!(*args)
|
397
|
+
# raise PlaybackError, "Playback failed with PLAYBACKSTATUS: #{playback.inspect}. The raw response was #{response.inspect}."
|
398
|
+
return_values = base_record_to_file(*args)
|
399
|
+
if return_values.first == :playback_error
|
400
|
+
raise PlaybackError, "Playback failed with PLAYBACKSTATUS: #{return_values.second.inspect}."
|
401
|
+
elsif return_values.first == :write_error
|
402
|
+
raise RecordError, "Record failed on write."
|
403
|
+
end
|
404
|
+
return_values.first
|
405
|
+
end
|
406
|
+
|
407
|
+
# this is a base methor record_to_file and record_to_file! and should only be used via those methods
|
408
|
+
#
|
409
|
+
def base_record_to_file(*args)
|
244
410
|
options = args.last.kind_of?(Hash) ? args.pop : {}
|
245
411
|
filename = args.shift || "/tmp/recording_%d"
|
246
412
|
|
@@ -271,15 +437,38 @@ module Adhearsion
|
|
271
437
|
escapedigits = options.delete(:escapedigits) || "#"
|
272
438
|
silence = options.delete(:silence) || 0
|
273
439
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
440
|
+
response_params = filename, format, escapedigits, maxduration, 0
|
441
|
+
response_values = []
|
442
|
+
|
443
|
+
if !options.has_key? :beep
|
444
|
+
response_params << 'BEEP'
|
445
|
+
elsif options[:beep]
|
446
|
+
play_soundfile options[:beep]
|
447
|
+
playback_response = get_variable('PLAYBACKSTATUS')
|
448
|
+
if playback_response != PLAYBACK_SUCCESS
|
449
|
+
response_values << :playback_error
|
450
|
+
response_values << playback_response
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
if silence > 0
|
455
|
+
response_params << "s=#{silence}"
|
278
456
|
end
|
279
457
|
|
280
|
-
|
458
|
+
resp = response 'RECORD FILE', *response_params
|
459
|
+
# If the user hangs up before the recording is entered, -1 is returned by asterisk and RECORDED_FILE
|
281
460
|
# will not contain the name of the file, even though it IS in fact recorded.
|
282
|
-
|
461
|
+
if resp.match /hangup/
|
462
|
+
response_values << :hangup
|
463
|
+
elsif resp.match /writefile/
|
464
|
+
response_values << :write_error
|
465
|
+
elsif resp.match /dtmf/
|
466
|
+
response_values << :success_dtmf
|
467
|
+
elsif resp.match /timeout/
|
468
|
+
response_values << :success_timeout
|
469
|
+
end
|
470
|
+
|
471
|
+
response_values
|
283
472
|
end
|
284
473
|
|
285
474
|
# Simulates pressing the specified digits over the current channel. Can be used to
|
@@ -488,6 +677,8 @@ module Adhearsion
|
|
488
677
|
# # or when the "0" key is pressed.
|
489
678
|
# input 3, :play => "you-sound-cute"
|
490
679
|
# input :play => ["if-this-is-correct-press", 1, "otherwise-press", 2]
|
680
|
+
# input :interruptible => false, :play => ["you-cannot-interrupt-this-message"] # Disallow DTMF (keypad) interruption
|
681
|
+
# # until after all files are played.
|
491
682
|
#
|
492
683
|
# When specifying files to play, the playback of the sequence of files will stop
|
493
684
|
# immediately when the user presses the first digit.
|
@@ -503,12 +694,9 @@ module Adhearsion
|
|
503
694
|
# @return [String] The keypad input received. An empty string is returned in the
|
504
695
|
# absense of input. If the :accept_key argument was pressed, it
|
505
696
|
# will not appear in the output.
|
506
|
-
def input(*args)
|
507
|
-
options = args.last.kind_of?(Hash) ? args.pop : {}
|
508
|
-
number_of_digits = args.shift
|
509
|
-
|
697
|
+
def input(*args, &block)
|
510
698
|
begin
|
511
|
-
input!
|
699
|
+
input! *args, &block
|
512
700
|
rescue PlaybackError => e
|
513
701
|
ahn_log.agi.warn { e }
|
514
702
|
retry # If sound playback fails, play the remaining sound files and wait for digits
|
@@ -519,11 +707,25 @@ module Adhearsion
|
|
519
707
|
#
|
520
708
|
# @return (see #input)
|
521
709
|
# @raise [Adhearsion::VoIP::PlaybackError] If a sound file cannot be played
|
522
|
-
def input!(*args)
|
710
|
+
def input!(*args, &block)
|
523
711
|
options = args.last.kind_of?(Hash) ? args.pop : {}
|
524
712
|
number_of_digits = args.shift
|
525
713
|
|
526
714
|
options[:play] = [*options[:play]].compact
|
715
|
+
|
716
|
+
if options.has_key?(:interruptible) && options[:interruptible] == false
|
717
|
+
play_command = :play!
|
718
|
+
else
|
719
|
+
options[:interruptible] = true
|
720
|
+
play_command = :interruptible_play!
|
721
|
+
end
|
722
|
+
|
723
|
+
if options.has_key? :speak
|
724
|
+
raise ArgumentError unless options[:speak].is_a? Hash
|
725
|
+
raise ArgumentError, 'Must include a text string when requesting TTS fallback' unless options[:speak].has_key?(:text)
|
726
|
+
options[:speak][:interruptible] = options[:interruptible]
|
727
|
+
end
|
728
|
+
|
527
729
|
timeout = options[:timeout]
|
528
730
|
terminating_key = options[:accept_key]
|
529
731
|
terminating_key = if terminating_key
|
@@ -533,20 +735,28 @@ module Adhearsion
|
|
533
735
|
end
|
534
736
|
|
535
737
|
if number_of_digits && number_of_digits < 0
|
536
|
-
ahn_log.agi.warn "Giving -1 to input
|
537
|
-
"argument to
|
738
|
+
ahn_log.agi.warn "Giving -1 to #input is now deprecated. Do not specify a first " +
|
739
|
+
"argument to allow unlimited digits." if number_of_digits == -1
|
538
740
|
raise ArgumentError, "The number of digits must be positive!"
|
539
741
|
end
|
540
742
|
|
541
743
|
buffer = ''
|
542
744
|
if options[:play].any?
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
745
|
+
begin
|
746
|
+
# Consume the sound files one at a time. In the event of playback
|
747
|
+
# failure, this tells us which files remain unplayed.
|
748
|
+
while file = options[:play].shift
|
749
|
+
key = send play_command, file
|
750
|
+
key = nil if play_command == :play!
|
751
|
+
break if key
|
752
|
+
end
|
753
|
+
rescue PlaybackError
|
754
|
+
raise unless options[:speak]
|
755
|
+
key = speak options[:speak].delete(:text), options[:speak]
|
548
756
|
end
|
549
757
|
key ||= ''
|
758
|
+
elsif options[:speak]
|
759
|
+
key = speak(options[:speak].delete(:text), options[:speak]) || ''
|
550
760
|
else
|
551
761
|
key = wait_for_digit timeout || -1
|
552
762
|
end
|
@@ -563,6 +773,7 @@ module Adhearsion
|
|
563
773
|
buffer << key
|
564
774
|
return buffer if number_of_digits && number_of_digits == buffer.length
|
565
775
|
end
|
776
|
+
return buffer if block_given? && yield(buffer)
|
566
777
|
key = wait_for_digit(timeout || -1)
|
567
778
|
end
|
568
779
|
end
|
@@ -642,10 +853,80 @@ module Adhearsion
|
|
642
853
|
not last_dial_successful?
|
643
854
|
end
|
644
855
|
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
856
|
+
##
|
857
|
+
# @param [#to_s] text to speak using the TTS engine
|
858
|
+
# @param [Hash] options
|
859
|
+
# @param options [Symbol] :engine the engine to use. Currently supported engines are :cepstral and :unimrcp
|
860
|
+
# @param options [String] :barge_in_digits digits to allow the TTS to be interrupted with
|
861
|
+
#
|
862
|
+
def speak(text, options = {})
|
863
|
+
engine = options.delete(:engine) || AHN_CONFIG.asterisk.speech_engine || :none
|
864
|
+
options[:interruptible] = false unless options.has_key?(:interruptible)
|
865
|
+
SpeechEngines.send(engine, self, text.to_s, options)
|
866
|
+
end
|
867
|
+
|
868
|
+
module SpeechEngines
|
869
|
+
class InvalidSpeechEngine < StandardError; end
|
870
|
+
|
871
|
+
class << self
|
872
|
+
def cepstral(call, text, options = {})
|
873
|
+
# We need to aggressively escape commas so app_swift does not
|
874
|
+
# think they are arguments.
|
875
|
+
text.gsub! /,/, '\\\\\\,'
|
876
|
+
command = ['Swift', text]
|
877
|
+
|
878
|
+
if options[:interrupt_digits]
|
879
|
+
ahn_log.agi.warn 'Cepstral does not support specifying interrupt digits'
|
880
|
+
options[:interruptible] = true
|
881
|
+
end
|
882
|
+
# Wait for 1ms after speaking and collect no more than 1 digit
|
883
|
+
command += [1, 1] if options[:interruptible]
|
884
|
+
call.execute *command
|
885
|
+
call.get_variable('SWIFT_DTMF')
|
886
|
+
end
|
887
|
+
|
888
|
+
def unimrcp(call, text, options = {})
|
889
|
+
# app_unimrcp strips quotes, which will already be stripped by the AGI parser.
|
890
|
+
# To work around this bug, we have to actually quote the arguments twice, once
|
891
|
+
# in this method and again inside #execute.
|
892
|
+
# Example from the logs:
|
893
|
+
# 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>"
|
894
|
+
# [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>)
|
895
|
+
# [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>
|
896
|
+
command = ['MRCPSynth', text.gsub(/["\\]/) { |m| "\\#{m}" }]
|
897
|
+
args = []
|
898
|
+
if options[:interrupt_digits]
|
899
|
+
args << "i=#{options[:interrupt_digits]}"
|
900
|
+
else
|
901
|
+
args << "i=any" if options[:interruptible]
|
902
|
+
end
|
903
|
+
command << args.join('&') unless args.empty?
|
904
|
+
value = call.inline_return_value(call.execute *command)
|
905
|
+
value.to_i.chr unless value.nil?
|
906
|
+
end
|
907
|
+
|
908
|
+
def tropo(call, text, options = {})
|
909
|
+
command = ['Ask', text]
|
910
|
+
args = {}
|
911
|
+
args[:terminator] = options[:interrupt_digits].split('').join(',') if options[:interrupt_digits]
|
912
|
+
args[:bargein] = options[:interruptible] if options.has_key?(:interruptible)
|
913
|
+
command << args.to_json unless args.empty?
|
914
|
+
value = JSON.parse call.raw_response(*command).sub(/^200 result=/, '')
|
915
|
+
value['interpretation']
|
916
|
+
end
|
917
|
+
|
918
|
+
def festival(text, call, options = {})
|
919
|
+
raise NotImplementedError
|
920
|
+
end
|
921
|
+
|
922
|
+
def none(text, call, options = {})
|
923
|
+
raise InvalidSpeechEngine, "No speech engine selected. You must specify one in your Adhearsion config file."
|
924
|
+
end
|
925
|
+
|
926
|
+
def method_missing(engine_name, text, options = {})
|
927
|
+
raise InvalidSpeechEngine, "Unsupported speech engine #{engine_name} for speaking '#{text}'"
|
928
|
+
end
|
929
|
+
end
|
649
930
|
end
|
650
931
|
|
651
932
|
# A high-level way of enabling features you create/uncomment from features.conf.
|
@@ -719,13 +1000,7 @@ module Adhearsion
|
|
719
1000
|
#
|
720
1001
|
# @see: http://www.voip-info.org/wiki/view/get+variable Asterisk Get Variable
|
721
1002
|
def get_variable(variable_name)
|
722
|
-
|
723
|
-
case result
|
724
|
-
when "200 result=0"
|
725
|
-
return nil
|
726
|
-
when /^200 result=1 \((.*)\)$/
|
727
|
-
return $LAST_PAREN_MATCH
|
728
|
-
end
|
1003
|
+
inline_result_with_return_value response "GET VARIABLE", variable_name
|
729
1004
|
end
|
730
1005
|
|
731
1006
|
# Pass information back to the asterisk dial plan.
|
@@ -740,7 +1015,7 @@ module Adhearsion
|
|
740
1015
|
#
|
741
1016
|
# @see http://www.voip-info.org/wiki/view/set+variable Asterisk Set Variable
|
742
1017
|
def set_variable(variable_name, value)
|
743
|
-
response("SET VARIABLE", variable_name, value) ==
|
1018
|
+
response("SET VARIABLE", variable_name, value) == AGI_SUCCESSFUL_RESPONSE
|
744
1019
|
end
|
745
1020
|
|
746
1021
|
# Issue the command to add a custom SIP header to the current call channel
|
@@ -753,7 +1028,7 @@ module Adhearsion
|
|
753
1028
|
#
|
754
1029
|
# @see http://www.voip-info.org/wiki/index.php?page=Asterisk+cmd+SIPAddHeader Asterisk SIPAddHeader
|
755
1030
|
def sip_add_header(header, value)
|
756
|
-
execute("SIPAddHeader", "#{header}: #{value}") ==
|
1031
|
+
execute("SIPAddHeader", "#{header}: #{value}") == AGI_SUCCESSFUL_RESPONSE
|
757
1032
|
end
|
758
1033
|
|
759
1034
|
# Issue the command to fetch a SIP header from the current call channel
|
@@ -965,11 +1240,18 @@ module Adhearsion
|
|
965
1240
|
# @return [String, nil] digit pressed, or nil if none
|
966
1241
|
#
|
967
1242
|
def interruptible_play(*files)
|
1243
|
+
result = nil
|
968
1244
|
files.flatten.each do |file|
|
969
|
-
|
970
|
-
|
1245
|
+
begin
|
1246
|
+
result = interruptible_play!(file)
|
1247
|
+
rescue PlaybackError => e
|
1248
|
+
# Ignore this exception and play the next file
|
1249
|
+
ahn_log.agi.warn e.message
|
1250
|
+
ensure
|
1251
|
+
break if result
|
1252
|
+
end
|
971
1253
|
end
|
972
|
-
|
1254
|
+
result
|
973
1255
|
end
|
974
1256
|
|
975
1257
|
#
|
@@ -984,7 +1266,7 @@ module Adhearsion
|
|
984
1266
|
if result[:endpos].to_i <= startpos
|
985
1267
|
raise Adhearsion::VoIP::PlaybackError, "The sound file could not opened to stream. The parsed response was #{result.inspect}"
|
986
1268
|
end
|
987
|
-
return result[:digit]
|
1269
|
+
return result[:digit] if result.has_key? :digit
|
988
1270
|
end
|
989
1271
|
nil
|
990
1272
|
end
|
@@ -1065,7 +1347,7 @@ module Adhearsion
|
|
1065
1347
|
# allows setting of the callerid number of the call
|
1066
1348
|
def set_caller_id_number(caller_id_num)
|
1067
1349
|
return unless caller_id_num
|
1068
|
-
raise ArgumentError, "Caller ID must be numeric" if caller_id_num.to_s !~
|
1350
|
+
raise ArgumentError, "Caller ID must be numeric" if caller_id_num.to_s !~ /^\+?\d+$/
|
1069
1351
|
variable "CALLERID(num)" => caller_id_num
|
1070
1352
|
end
|
1071
1353
|
|
@@ -1134,7 +1416,7 @@ module Adhearsion
|
|
1134
1416
|
raise ArgumentError, "Can't coerce nil into AGI response! This could be a bug!" unless response_string
|
1135
1417
|
params = {}
|
1136
1418
|
digit, endpos = response_string.match(/^#{response_prefix}(-?\d+) endpos=(\d+)/).values_at 1, 2
|
1137
|
-
params[:digit] = digit.to_i.chr
|
1419
|
+
params[:digit] = digit.to_i.chr unless digit == "0" || digit.to_s == "-1"
|
1138
1420
|
params[:endpos] = endpos.to_i if endpos
|
1139
1421
|
params
|
1140
1422
|
end
|
@@ -1166,22 +1448,25 @@ module Adhearsion
|
|
1166
1448
|
end
|
1167
1449
|
end
|
1168
1450
|
|
1169
|
-
|
1451
|
+
# Instruct Asterisk to play a sound file to the channel
|
1452
|
+
def play_soundfile(argument)
|
1170
1453
|
execute(:playback, argument)
|
1171
1454
|
get_variable('PLAYBACKSTATUS') == PLAYBACK_SUCCESS
|
1172
1455
|
end
|
1456
|
+
alias :play_string :play_soundfile
|
1173
1457
|
|
1174
|
-
# Like
|
1458
|
+
# Like play_soundfile, but this will raise Exceptions if there's a problem.
|
1175
1459
|
#
|
1176
1460
|
# @return [true]
|
1177
1461
|
# @raise [Adhearsion::VoIP::PlaybackError] If a sound file cannot be played
|
1178
1462
|
# @see http://www.voip-info.org/wiki/view/Asterisk+cmd+Playback More information on the Asterisk Playback command
|
1179
|
-
def
|
1463
|
+
def play_soundfile!(argument)
|
1180
1464
|
response = execute :playback, argument
|
1181
1465
|
playback = get_variable 'PLAYBACKSTATUS'
|
1182
1466
|
return true if playback == PLAYBACK_SUCCESS
|
1183
1467
|
raise PlaybackError, "Playback failed with PLAYBACKSTATUS: #{playback.inspect}. The raw response was #{response.inspect}."
|
1184
1468
|
end
|
1469
|
+
alias :play_string! :play_soundfile!
|
1185
1470
|
|
1186
1471
|
def play_sound_files_for_menu(menu_instance, sound_files)
|
1187
1472
|
digit = nil
|
@@ -1638,37 +1923,6 @@ module Adhearsion
|
|
1638
1923
|
end
|
1639
1924
|
end
|
1640
1925
|
|
1641
|
-
module SpeechEngines
|
1642
|
-
|
1643
|
-
class InvalidSpeechEngine < StandardError; end
|
1644
|
-
|
1645
|
-
class << self
|
1646
|
-
def cepstral(text)
|
1647
|
-
puts "in ceptral"
|
1648
|
-
puts escape(text)
|
1649
|
-
end
|
1650
|
-
|
1651
|
-
def festival(text)
|
1652
|
-
raise NotImplementedError
|
1653
|
-
end
|
1654
|
-
|
1655
|
-
def none(text)
|
1656
|
-
raise InvalidSpeechEngine, "No speech engine selected. You must specify one in your Adhearsion config file."
|
1657
|
-
end
|
1658
|
-
|
1659
|
-
def method_missing(engine_name, text)
|
1660
|
-
raise InvalidSpeechEngine, "Unsupported speech engine #{engine_name} for speaking '#{text}'"
|
1661
|
-
end
|
1662
|
-
|
1663
|
-
private
|
1664
|
-
|
1665
|
-
def escape(text)
|
1666
|
-
"%p" % text
|
1667
|
-
end
|
1668
|
-
|
1669
|
-
end
|
1670
|
-
end
|
1671
|
-
|
1672
1926
|
end
|
1673
1927
|
end
|
1674
1928
|
end
|