punchblock 2.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGELOG.md +7 -0
- data/lib/punchblock.rb +1 -1
- data/lib/punchblock/component.rb +2 -0
- data/lib/punchblock/component/input.rb +9 -1
- data/lib/punchblock/component/output.rb +1 -1
- data/lib/punchblock/component/receive_fax.rb +24 -0
- data/lib/punchblock/component/send_fax.rb +62 -0
- data/lib/punchblock/connection/asterisk.rb +1 -1
- data/lib/punchblock/event/complete.rb +15 -1
- data/lib/punchblock/translator/asterisk.rb +30 -15
- data/lib/punchblock/translator/asterisk/call.rb +13 -27
- data/lib/punchblock/translator/asterisk/component.rb +4 -7
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +1 -5
- data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +8 -9
- data/lib/punchblock/translator/asterisk/component/input.rb +2 -3
- data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +9 -9
- data/lib/punchblock/translator/asterisk/component/output.rb +134 -39
- data/lib/punchblock/translator/asterisk/component/record.rb +2 -3
- data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +2 -3
- data/lib/punchblock/translator/dtmf_recognizer.rb +2 -4
- data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +6 -1
- data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
- data/lib/punchblock/translator/freeswitch/component/output.rb +12 -10
- data/lib/punchblock/translator/freeswitch/component/tts_output.rb +1 -1
- data/lib/punchblock/version.rb +1 -1
- data/spec/punchblock/component/input_spec.rb +91 -0
- data/spec/punchblock/component/output_spec.rb +1 -2
- data/spec/punchblock/component/receive_fax_spec.rb +111 -0
- data/spec/punchblock/component/send_fax_spec.rb +110 -0
- data/spec/punchblock/connection/asterisk_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +53 -79
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +0 -3
- data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +2 -2
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +6 -6
- data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +3 -3
- data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +9 -11
- data/spec/punchblock/translator/asterisk/component/output_spec.rb +902 -28
- data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/component_spec.rb +2 -9
- data/spec/punchblock/translator/asterisk_spec.rb +42 -94
- data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +5 -5
- data/spec/punchblock/translator/freeswitch/component/output_spec.rb +7 -3
- data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +17 -5
- 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)
|
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.
|
19
|
-
call.register_component output_component
|
20
|
-
fut = output_component.
|
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
|
-
|
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
|
-
|
62
|
+
process_dtmf event['Digit']
|
64
63
|
end
|
65
64
|
end
|
66
65
|
|
67
66
|
def unregister_dtmf_event_handler
|
68
|
-
call.
|
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
|
-
|
21
|
+
process_dtmf event['Digit']
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
26
25
|
def unregister_dtmf_event_handler
|
27
|
-
call.
|
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',
|
32
|
+
execute_app 'SynthAndRecog', render_doc, grammars
|
32
33
|
end
|
33
34
|
|
34
|
-
def
|
35
|
-
output_node.render_documents.
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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, :
|
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
|
-
|
33
|
-
|
34
|
+
validate_audio_only
|
35
|
+
setup_for_native
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
64
|
+
send_progress_if_necessary
|
58
65
|
send_ref
|
59
|
-
|
60
|
-
|
66
|
+
repeat_times.times do
|
67
|
+
render_with_unimrcp(*render_docs)
|
68
|
+
end
|
61
69
|
when :swift
|
62
|
-
|
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
|
86
|
-
|
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
|
137
|
+
names[node] = path_for_audio_node node
|
90
138
|
when String
|
91
|
-
|
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
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
102
|
-
@component_node.render_documents
|
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 =
|
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
|
-
|
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
|
-
|
22
|
+
send_complete_event complete_reason
|
24
23
|
end
|
25
|
-
call.
|
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
|
@@ -17,17 +17,19 @@ module Punchblock
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def filenames
|
20
|
-
@filenames ||= @component_node.render_documents.
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|