adhearsion 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +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
|