punchblock 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -1
  3. data/CHANGELOG.md +7 -0
  4. data/lib/punchblock.rb +1 -1
  5. data/lib/punchblock/component.rb +2 -0
  6. data/lib/punchblock/component/input.rb +9 -1
  7. data/lib/punchblock/component/output.rb +1 -1
  8. data/lib/punchblock/component/receive_fax.rb +24 -0
  9. data/lib/punchblock/component/send_fax.rb +62 -0
  10. data/lib/punchblock/connection/asterisk.rb +1 -1
  11. data/lib/punchblock/event/complete.rb +15 -1
  12. data/lib/punchblock/translator/asterisk.rb +30 -15
  13. data/lib/punchblock/translator/asterisk/call.rb +13 -27
  14. data/lib/punchblock/translator/asterisk/component.rb +4 -7
  15. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +1 -5
  16. data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +8 -9
  17. data/lib/punchblock/translator/asterisk/component/input.rb +2 -3
  18. data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +9 -9
  19. data/lib/punchblock/translator/asterisk/component/output.rb +134 -39
  20. data/lib/punchblock/translator/asterisk/component/record.rb +2 -3
  21. data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +2 -3
  22. data/lib/punchblock/translator/dtmf_recognizer.rb +2 -4
  23. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +6 -1
  24. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
  25. data/lib/punchblock/translator/freeswitch/component/output.rb +12 -10
  26. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +1 -1
  27. data/lib/punchblock/version.rb +1 -1
  28. data/spec/punchblock/component/input_spec.rb +91 -0
  29. data/spec/punchblock/component/output_spec.rb +1 -2
  30. data/spec/punchblock/component/receive_fax_spec.rb +111 -0
  31. data/spec/punchblock/component/send_fax_spec.rb +110 -0
  32. data/spec/punchblock/connection/asterisk_spec.rb +1 -1
  33. data/spec/punchblock/translator/asterisk/call_spec.rb +53 -79
  34. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +0 -3
  35. data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +1 -1
  36. data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +2 -2
  37. data/spec/punchblock/translator/asterisk/component/input_spec.rb +6 -6
  38. data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +3 -3
  39. data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +9 -11
  40. data/spec/punchblock/translator/asterisk/component/output_spec.rb +902 -28
  41. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -1
  42. data/spec/punchblock/translator/asterisk/component_spec.rb +2 -9
  43. data/spec/punchblock/translator/asterisk_spec.rb +42 -94
  44. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +5 -5
  45. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +7 -3
  46. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +17 -5
  47. metadata +67 -61
@@ -15,20 +15,16 @@ module Punchblock
15
15
  send_ref
16
16
  rescue RubyAMI::Error
17
17
  set_node_response false
18
- terminate
19
18
  rescue ChannelGoneError
20
19
  set_node_response ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{call_id}", call_id)
21
- terminate
22
20
  end
23
- exclusive :execute
24
21
 
25
22
  def handle_ami_event(event)
26
23
  if event.name == 'AsyncAGI' && event['SubEvent'] == 'Exec'
27
- send_complete_event success_reason(event), nil, false
24
+ send_complete_event success_reason(event)
28
25
  if @component_node.name == 'ASYNCAGI BREAK' && @call.channel_var('PUNCHBLOCK_END_ON_ASYNCAGI_BREAK')
29
26
  @call.handle_hangup_event
30
27
  end
31
- terminate
32
28
  end
33
29
  end
34
30
 
@@ -15,15 +15,15 @@ module Punchblock
15
15
 
16
16
  @output_incomplete = true
17
17
 
18
- output_component = Output.new_link(output_command, @call)
19
- call.register_component output_component
20
- fut = output_component.future.execute
18
+ @output_component = Output.new(output_command, @call)
19
+ call.register_component @output_component
20
+ fut = Celluloid::Future.new { @output_component.execute }
21
21
 
22
- case output_command.response
22
+ case @output_command.response
23
23
  when Ref
24
24
  send_ref
25
25
  else
26
- set_node_response output_command.response
26
+ set_node_response @output_command.response
27
27
  end
28
28
 
29
29
  if @component_node.barge_in
@@ -41,7 +41,7 @@ module Punchblock
41
41
 
42
42
  def process_dtmf(digit)
43
43
  if @component_node.barge_in && @output_incomplete
44
- call.async.redirect_back
44
+ @output_component.stop_by_redirect Punchblock::Event::Complete::Stop.new
45
45
  @barged = true
46
46
  end
47
47
  super
@@ -58,14 +58,13 @@ module Punchblock
58
58
  end
59
59
 
60
60
  def register_dtmf_event_handler
61
- component = current_actor
62
61
  @dtmf_handler_id = call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
63
- component.process_dtmf event['Digit']
62
+ process_dtmf event['Digit']
64
63
  end
65
64
  end
66
65
 
67
66
  def unregister_dtmf_event_handler
68
- call.async.unregister_handler :ami, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id)
67
+ call.unregister_handler :ami, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id)
69
68
  end
70
69
  end
71
70
  end
@@ -17,14 +17,13 @@ module Punchblock
17
17
  private
18
18
 
19
19
  def register_dtmf_event_handler
20
- component = current_actor
21
20
  call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
22
- component.process_dtmf event['Digit']
21
+ process_dtmf event['Digit']
23
22
  end
24
23
  end
25
24
 
26
25
  def unregister_dtmf_event_handler
27
- call.async.unregister_handler :ami, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id)
26
+ call.unregister_handler :ami, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id)
28
27
  end
29
28
  end
30
29
  end
@@ -14,6 +14,7 @@ module Punchblock
14
14
  raise OptionError, "The renderer #{renderer} is unsupported." unless renderer == 'unimrcp'
15
15
  raise OptionError, "The recognizer #{recognizer} is unsupported." unless recognizer == 'unimrcp'
16
16
  raise OptionError, 'An SSML document is required.' unless output_node.render_documents.count > 0
17
+ raise OptionError, 'Only one document is allowed.' if output_node.render_documents.count > 1
17
18
  raise OptionError, 'A grammar is required.' unless input_node.grammars.count > 0
18
19
 
19
20
  super
@@ -28,17 +29,16 @@ module Punchblock
28
29
  end
29
30
 
30
31
  def execute_unimrcp_app
31
- execute_app 'SynthAndRecog', render_docs, grammars
32
+ execute_app 'SynthAndRecog', render_doc, grammars
32
33
  end
33
34
 
34
- def render_docs
35
- output_node.render_documents.map do |d|
36
- if d.content_type
37
- d.value.to_doc.to_s
38
- else
39
- d.url
40
- end
41
- end.join ','
35
+ def render_doc
36
+ d = output_node.render_documents.first
37
+ if d.content_type
38
+ d.value.to_doc.to_s
39
+ else
40
+ d.url
41
+ end
42
42
  end
43
43
 
44
44
  def unimrcp_app_options
@@ -16,50 +16,58 @@ module Punchblock
16
16
 
17
17
  def execute
18
18
  raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value
19
- raise OptionError, 'Only a single document is supported.' unless @component_node.render_documents.size == 1
20
19
  raise OptionError, 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :voice
21
20
 
22
- [:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
21
+ [:start_offset, :start_paused, :repeat_interval, :max_time].each do |opt|
23
22
  raise OptionError, "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
24
23
  end
25
24
 
26
- early = !@call.answered?
25
+ @early = !@call.answered?
27
26
 
28
27
  rendering_engine = @component_node.renderer || :asterisk
29
28
 
29
+ repeat_times = @component_node.repeat_times || 1
30
+ repeat_times = 1000 if repeat_times.zero?
31
+
30
32
  case rendering_engine.to_sym
31
33
  when :asterisk
32
- raise OptionError, "A voice value is unsupported on Asterisk." if @component_node.voice
33
- raise OptionError, 'Interrupt digits are not allowed with early media.' if early && @component_node.interrupt_on
34
+ validate_audio_only
35
+ setup_for_native
34
36
 
35
- case @component_node.interrupt_on
36
- when :any, :dtmf
37
- interrupt = true
37
+ repeat_times.times do
38
+ render_docs.each do |doc|
39
+ playback(filenames(doc).values) || raise(PlaybackError)
40
+ end
38
41
  end
39
-
40
- path = filenames.join '&'
41
-
42
- @call.send_progress if early
43
-
44
- if interrupt
45
- output_component = current_actor
46
- call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
47
- output_component.stop_by_redirect finish_reason
42
+ when :native_or_unimrcp
43
+ setup_for_native
44
+
45
+ repeat_times.times do
46
+ render_docs.each do |doc|
47
+ doc.value.children.each do |node|
48
+ case node
49
+ when RubySpeech::SSML::Audio
50
+ playback([path_for_audio_node(node)]) || render_with_unimrcp(fallback_doc(doc, node))
51
+ when String
52
+ if node.include?(' ')
53
+ render_with_unimrcp(copied_doc(doc, node))
54
+ else
55
+ playback([node]) || render_with_unimrcp(copied_doc(doc, node))
56
+ end
57
+ else
58
+ render_with_unimrcp(copied_doc(doc, node.node))
59
+ end
60
+ end
48
61
  end
49
62
  end
50
-
51
- send_ref
52
-
53
- opts = early ? "#{path},noanswer" : path
54
- @call.execute_agi_command 'EXEC Playback', opts
55
- raise PlaybackError if @call.channel_var('PLAYBACKSTATUS') == 'FAILED'
56
63
  when :unimrcp
57
- @call.send_progress if early
64
+ send_progress_if_necessary
58
65
  send_ref
59
- UniMRCPApp.new('MRCPSynth', render_doc, mrcpsynth_options).execute @call
60
- raise UniMRCPError if @call.channel_var('SYNTHSTATUS') == 'ERROR'
66
+ repeat_times.times do
67
+ render_with_unimrcp(*render_docs)
68
+ end
61
69
  when :swift
62
- @call.send_progress if early
70
+ send_progress_if_necessary
63
71
  send_ref
64
72
  @call.execute_agi_command 'EXEC Swift', swift_doc
65
73
  else
@@ -80,26 +88,113 @@ module Punchblock
80
88
  with_error 'option error', e.message
81
89
  end
82
90
 
91
+ def stop_by_redirect(*args)
92
+ @stopped = true
93
+ super
94
+ end
95
+
83
96
  private
84
97
 
85
- def filenames
86
- @filenames ||= render_doc.children.map do |node|
98
+ def setup_for_native
99
+ raise OptionError, "A voice value is unsupported on Asterisk." if @component_node.voice
100
+ raise OptionError, 'Interrupt digits are not allowed with early media.' if @early && @component_node.interrupt_on
101
+
102
+ case @component_node.interrupt_on
103
+ when :any, :dtmf
104
+ interrupt = true
105
+ end
106
+
107
+ send_progress_if_necessary
108
+
109
+ if interrupt
110
+ call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
111
+ stop_by_redirect finish_reason
112
+ end
113
+ end
114
+
115
+ send_ref
116
+ end
117
+
118
+ def send_progress_if_necessary
119
+ @call.send_progress if @early
120
+ end
121
+
122
+ def validate_audio_only
123
+ render_docs.each do |doc|
124
+ filenames doc, -> { raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.' }
125
+ end
126
+ end
127
+
128
+ def path_for_audio_node(node)
129
+ node.src.sub('file://', '').gsub(/\.[^\.]*$/, '')
130
+ end
131
+
132
+ def filenames(doc, check_audio_only_policy = -> {})
133
+ names = {}
134
+ doc.value.children.each do |node|
87
135
  case node
88
136
  when RubySpeech::SSML::Audio
89
- node.src.sub('file://', '').gsub(/\.[^\.]*$/, '')
137
+ names[node] = path_for_audio_node node
90
138
  when String
91
- raise if node.include?(' ')
92
- node
139
+ check_audio_only_policy.call if node.include?(' ')
140
+ names[nil] = node
141
+ else
142
+ check_audio_only_policy.call
143
+ end
144
+ end
145
+ names
146
+ end
147
+
148
+ def playback_options(paths)
149
+ opts = paths.join '&'
150
+ opts << ",noanswer" if @early
151
+ opts
152
+ end
153
+
154
+ def playback(paths)
155
+ return true if @stopped
156
+ @call.execute_agi_command 'EXEC Playback', playback_options(paths)
157
+ @call.channel_var('PLAYBACKSTATUS') != 'FAILED'
158
+ end
159
+
160
+ def fallback_doc(original, failed_audio_node)
161
+ children = failed_audio_node.nokogiri_children
162
+ copied_doc original, children
163
+ end
164
+
165
+ def copied_doc(original, elements)
166
+ doc = RubySpeech::SSML.draw do
167
+ if Nokogiri.jruby?
168
+ self.write_attr 'version', original.value['version']
169
+ self.write_attr 'xml:lang', original.value['xml:lang']
93
170
  else
94
- raise
171
+ original.value.attributes.each do |name, value|
172
+ attr_name = value.namespace && value.namespace.prefix ? [value.namespace.prefix, name].join(':') : name
173
+ self.write_attr attr_name, value
174
+ end
95
175
  end
96
- end.compact
97
- rescue
98
- raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.'
176
+
177
+ add_child Nokogiri.jruby? ? elements : elements.to_xml
178
+ end
179
+ Punchblock::Component::Output::Document.new(value: doc)
180
+ end
181
+
182
+ def render_with_unimrcp(*docs)
183
+ docs.each do |doc|
184
+ return if @stopped
185
+ UniMRCPApp.new('MRCPSynth', doc.value.to_s, mrcpsynth_options).execute @call
186
+ raise UniMRCPError if @call.channel_var('SYNTHSTATUS') == 'ERROR'
187
+ end
99
188
  end
100
189
 
101
- def render_doc
102
- @component_node.render_documents.first.value
190
+ def render_docs
191
+ @component_node.render_documents
192
+ end
193
+
194
+ def concatenated_render_doc
195
+ render_docs.inject RubySpeech::SSML.draw do |doc, argument|
196
+ doc + argument.value
197
+ end
103
198
  end
104
199
 
105
200
  def mrcpsynth_options
@@ -110,7 +205,7 @@ module Punchblock
110
205
  end
111
206
 
112
207
  def swift_doc
113
- doc = render_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
208
+ doc = concatenated_render_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
114
209
  doc << "|1|1" if [:any, :dtmf].include? @component_node.interrupt_on
115
210
  doc.insert 0, "#{@component_node.voice}^" if @component_node.voice
116
211
  doc
@@ -22,9 +22,8 @@ module Punchblock
22
22
 
23
23
  @format = @component_node.format || 'wav'
24
24
 
25
- component = current_actor
26
25
  call.register_tmp_handler :ami, :name => 'MonitorStop' do |event|
27
- component.finished
26
+ finished
28
27
  end
29
28
 
30
29
  if @component_node.start_beep
@@ -33,7 +32,7 @@ module Punchblock
33
32
 
34
33
  ami_client.send_action 'Monitor', 'Channel' => call.channel, 'File' => filename, 'Format' => @format, 'Mix' => true
35
34
  unless max_duration == -1
36
- after max_duration/1000 do
35
+ call.after max_duration/1000 do
37
36
  ami_client.send_action 'StopMonitor', 'Channel' => call.channel
38
37
  end
39
38
  end
@@ -18,11 +18,10 @@ module Punchblock
18
18
  end
19
19
 
20
20
  def stop_by_redirect(complete_reason)
21
- component_actor = current_actor
22
21
  call.register_handler :ami, lambda { |e| e['SubEvent'] == 'Start' }, :name => 'AsyncAGI' do |event|
23
- component_actor.async.send_complete_event complete_reason
22
+ send_complete_event complete_reason
24
23
  end
25
- call.async.redirect_back
24
+ call.redirect_back
26
25
  end
27
26
  end
28
27
  end
@@ -23,6 +23,8 @@ module Punchblock
23
23
  end
24
24
  end
25
25
 
26
+ include Celluloid
27
+
26
28
  def initialize(responder, grammar, initial_timeout = nil, inter_digit_timeout = nil, terminator = nil)
27
29
  @responder = responder
28
30
  self.initial_timeout = initial_timeout || -1
@@ -68,10 +70,6 @@ module Punchblock
68
70
  @matcher.match @buffer.dup
69
71
  end
70
72
 
71
- def after(*args, &block)
72
- @responder.after *args, &block
73
- end
74
-
75
73
  def initial_timeout=(other)
76
74
  raise OptionError, 'An initial timeout value that is negative (and not -1) is invalid.' if other < -1
77
75
  @initial_timeout = other
@@ -36,7 +36,6 @@ module Punchblock
36
36
 
37
37
  def validate
38
38
  raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value
39
- raise OptionError, 'Only a single document is supported.' unless @component_node.render_documents.size == 1
40
39
 
41
40
  [:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
42
41
  raise OptionError, "A #{opt} value is unsupported." if @component_node.send opt
@@ -48,6 +47,12 @@ module Punchblock
48
47
  end
49
48
  end
50
49
 
50
+ def concatenated_render_doc
51
+ @component_node.render_documents.inject RubySpeech::SSML.draw do |doc, argument|
52
+ doc + argument.value
53
+ end
54
+ end
55
+
51
56
  def finish_reason
52
57
  Punchblock::Component::Output::Complete::Finish.new
53
58
  end
@@ -12,7 +12,7 @@ module Punchblock
12
12
  end
13
13
 
14
14
  def document
15
- @component_node.render_documents.first.value.inner_text.to_s
15
+ concatenated_render_doc.inner_text.to_s
16
16
  end
17
17
  end
18
18
  end
@@ -17,17 +17,19 @@ module Punchblock
17
17
  end
18
18
 
19
19
  def filenames
20
- @filenames ||= @component_node.render_documents.first.value.children.map do |node|
21
- case node
22
- when RubySpeech::SSML::Audio
23
- node.src
24
- when String
25
- raise if node.include?(' ')
26
- node
27
- else
28
- raise
20
+ @filenames ||= @component_node.render_documents.map do |doc|
21
+ doc.value.children.map do |node|
22
+ case node
23
+ when RubySpeech::SSML::Audio
24
+ node.src
25
+ when String
26
+ raise if node.include?(' ')
27
+ node
28
+ else
29
+ raise
30
+ end
29
31
  end
30
- end.compact
32
+ end.compact.flatten
31
33
  rescue
32
34
  raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.'
33
35
  end