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.
Files changed (83) hide show
  1. data/.gitignore +3 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG +12 -0
  4. data/README.markdown +1 -10
  5. data/Rakefile +6 -28
  6. data/adhearsion.gemspec +21 -38
  7. data/app_generators/ahn/ahn_generator.rb +3 -4
  8. data/app_generators/ahn/templates/config/environment.rb +4 -0
  9. data/app_generators/ahn/templates/config/startup.rb +11 -5
  10. data/app_generators/ahn/templates/script/ahn +8 -0
  11. data/lib/adhearsion/cli.rb +5 -298
  12. data/lib/adhearsion/commands.rb +308 -0
  13. data/lib/adhearsion/events_support.rb +0 -20
  14. data/lib/adhearsion/initializer/asterisk.rb +0 -1
  15. data/lib/adhearsion/initializer/configuration.rb +4 -1
  16. data/lib/adhearsion/logging.rb +18 -3
  17. data/lib/adhearsion/script_ahn_loader.rb +30 -0
  18. data/lib/adhearsion/tasks/testing.rb +39 -8
  19. data/lib/adhearsion/version.rb +2 -2
  20. data/lib/adhearsion/voip/asterisk/agi_server.rb +15 -8
  21. data/lib/adhearsion/voip/asterisk/commands.rb +340 -86
  22. data/lib/adhearsion/voip/asterisk/manager_interface.rb +0 -2
  23. data/lib/adhearsion/voip/call.rb +11 -3
  24. data/lib/adhearsion/voip/commands.rb +5 -1
  25. data/lib/adhearsion/voip/dial_plan.rb +4 -0
  26. data/lib/theatre/invocation.rb +3 -0
  27. data/spec/{ahn_command_spec.rb → adhearsion/cli_spec.rb} +11 -0
  28. data/spec/{component_manager_spec.rb → adhearsion/component_manager_spec.rb} +0 -0
  29. data/spec/{constants_spec.rb → adhearsion/constants_spec.rb} +0 -0
  30. data/spec/{drb_spec.rb → adhearsion/drb_spec.rb} +0 -0
  31. data/spec/{fixtures → adhearsion/fixtures}/dialplan.rb +0 -0
  32. data/spec/{foundation → adhearsion/foundation}/event_socket_spec.rb +0 -0
  33. data/spec/{host_definitions_spec.rb → adhearsion/host_definitions_spec.rb} +0 -0
  34. data/spec/{initializer → adhearsion/initializer}/configuration_spec.rb +21 -0
  35. data/spec/{initializer → adhearsion/initializer}/loading_spec.rb +0 -0
  36. data/spec/{initializer → adhearsion/initializer}/paths_spec.rb +0 -0
  37. data/spec/{initialization_spec.rb → adhearsion/initializer_spec.rb} +0 -0
  38. data/spec/{logging_spec.rb → adhearsion/logging_spec.rb} +6 -0
  39. data/spec/{relationship_properties_spec.rb → adhearsion/relationship_properties_spec.rb} +0 -0
  40. data/spec/{voip → adhearsion/voip}/asterisk/agi_server_spec.rb +0 -0
  41. data/spec/{voip → adhearsion/voip}/asterisk/ami/ami_spec.rb +1 -0
  42. data/spec/{voip → adhearsion/voip}/asterisk/ami/lexer/ami_fixtures.yml +0 -0
  43. data/spec/{voip → adhearsion/voip}/asterisk/ami/lexer/lexer_story +0 -0
  44. data/spec/{voip → adhearsion/voip}/asterisk/ami/lexer/lexer_story.rb +0 -0
  45. data/spec/{voip → adhearsion/voip}/asterisk/ami/lexer/story_helper.rb +0 -0
  46. data/spec/{voip → adhearsion/voip}/asterisk/commands_spec.rb +549 -47
  47. data/spec/{voip → adhearsion/voip}/asterisk/config_file_generators/agents_spec.rb +0 -0
  48. data/spec/{voip → adhearsion/voip}/asterisk/config_file_generators/queues_spec.rb +0 -0
  49. data/spec/{voip → adhearsion/voip}/asterisk/config_file_generators/voicemail_spec.rb +0 -0
  50. data/spec/{voip → adhearsion/voip}/asterisk/config_manager_spec.rb +0 -0
  51. data/spec/{voip → adhearsion/voip}/asterisk/menu_command/calculated_match_spec.rb +0 -0
  52. data/spec/{voip → adhearsion/voip}/asterisk/menu_command/matchers_spec.rb +0 -0
  53. data/spec/{voip → adhearsion/voip}/call_routing_spec.rb +0 -0
  54. data/spec/{voip → adhearsion/voip}/dialplan_manager_spec.rb +0 -0
  55. data/spec/{voip → adhearsion/voip}/dsl/dialing_dsl_spec.rb +1 -1
  56. data/spec/{voip → adhearsion/voip}/dsl/dispatcher_spec.rb +0 -0
  57. data/spec/{voip → adhearsion/voip}/dsl/dispatcher_spec_helper.rb +0 -0
  58. data/spec/{voip → adhearsion/voip}/dsl/parser_spec.rb +0 -0
  59. data/spec/{voip → adhearsion/voip}/freeswitch/basic_connection_manager_spec.rb +0 -0
  60. data/spec/{voip → adhearsion/voip}/freeswitch/inbound_connection_manager_spec.rb +0 -0
  61. data/spec/{voip → adhearsion/voip}/freeswitch/oes_server_spec.rb +0 -0
  62. data/spec/{voip → adhearsion/voip}/numerical_string_spec.rb +0 -0
  63. data/spec/{voip → adhearsion/voip}/phone_number_spec.rb +0 -0
  64. data/spec/spec_helper.rb +36 -89
  65. data/spec/support/initializer_stubs.rb +47 -0
  66. data/spec/support/the_following_code.rb +3 -0
  67. data/{theatre-spec → spec/theatre}/dsl_examples/simple_before_call.rb +0 -0
  68. data/{theatre-spec → spec/theatre}/dsl_spec.rb +26 -0
  69. data/{theatre-spec → spec/theatre}/invocation_spec.rb +6 -10
  70. data/{theatre-spec → spec/theatre}/namespace_spec.rb +0 -0
  71. data/{theatre-spec → spec/theatre}/spec_helper_spec.rb +0 -0
  72. data/{theatre-spec → spec/theatre}/theatre_class_spec.rb +2 -5
  73. metadata +254 -271
  74. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.rb +0 -104
  75. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.yml +0 -2
  76. data/lib/adhearsion/voip/asterisk/super_manager.rb +0 -19
  77. data/spec/silence.rb +0 -10
  78. data/spec/voip/asterisk/ami/old_tests.rb +0 -204
  79. data/spec/voip/asterisk/ami/super_manager/super_manager_story +0 -25
  80. data/spec/voip/asterisk/ami/super_manager/super_manager_story.rb +0 -15
  81. data/spec/voip/asterisk/ami/super_manager/super_manager_story_helper.rb +0 -5
  82. data/theatre-spec/dsl_examples/dynamic_stomp.rb +0 -7
  83. data/theatre-spec/spec_helper.rb +0 -37
@@ -1,8 +1,8 @@
1
1
  module Adhearsion #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1 unless defined? MAJOR
4
- MINOR = 1 unless defined? MINOR
5
- TINY = 1 unless defined? 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
- ahn_log.agi.debug "Handling call with variables #{call.variables.inspect}"
28
+ ahn_log.agi.debug "Handling call with variables #{call.variables.inspect}"
29
29
 
30
- return DialPlan::ConfirmationManager.handle(call) if DialPlan::ConfirmationManager.confirmation_call?(call)
30
+ return DialPlan::ConfirmationManager.handle(call) if DialPlan::ConfirmationManager.confirmation_call?(call)
31
31
 
32
- # This is what happens 99.9% of the time.
32
+ # This is what happens 99.9% of the time.
33
33
 
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}"
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
- call.hangup!
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
- # TBD: (may have more hooks than what Jay has defined in hooks.rb)
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(message + "\n")
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.call(arg.to_s) }.join(' '))
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
- result = raw_response(%{EXEC %s "%s"} % [ application,
148
- arguments.join(%{"#{AHN_CONFIG.asterisk.argument_delimiter}"}) ])
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 = raw_response("VERBOSE \"#{message}\" #{level}")
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) || play_string(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) || play_string!(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 record(*args)
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
- if (silence > 0)
275
- response("RECORD FILE", filename, format, escapedigits, maxduration, 0, "BEEP", "s=#{silence}")
276
- else
277
- response("RECORD FILE", filename, format, escapedigits, maxduration, 0, "BEEP")
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
- # If the user hangs up before the recording is entered, -1 is returned and RECORDED_FILE
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
- filename + "." + format
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! number_of_digits, options
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() is now deprecated. Don't specify a first " +
537
- "argument to simulate unlimited digits." if number_of_digits == -1
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
- # Consume the sound files one at a time. In the event of playback failure, this
544
- # tells us which files remain unplayed.
545
- while file = options[:play].shift
546
- key = interruptible_play! file
547
- break if key
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
- # This feature is presently experimental! Do not use it!
646
- def speak(text, engine=:none)
647
- engine = AHN_CONFIG.asterisk.speech_engine || engine
648
- execute SpeechEngines.send(engine, text)
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
- result = response("GET VARIABLE", variable_name)
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) == "200 result=1"
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}") == "200 result=1"
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
- result = result_digit_from response("STREAM FILE", file, "1234567890*#")
970
- return result if result != 0.chr
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
- nil
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] unless result[:digit] == 0.chr
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 !~ /^\d+$/
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 if digit && digit.to_s != "-1"
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
- def play_string(argument)
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 play_string(), but this will raise Exceptions if there's a problem.
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 play_string!(argument)
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