adhearsion 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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