punchblock 2.1.1 → 2.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 (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