punchblock 1.9.4 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -2
- data/CHANGELOG.md +17 -0
- data/Gemfile +1 -0
- data/Guardfile +4 -0
- data/README.markdown +6 -0
- data/Rakefile +16 -0
- data/benchmarks/ami_event_name_comparison.rb +14 -0
- data/benchmarks/channel.rb +27 -0
- data/lib/punchblock/client.rb +2 -6
- data/lib/punchblock/command/accept.rb +3 -24
- data/lib/punchblock/command/answer.rb +3 -24
- data/lib/punchblock/command/dial.rb +24 -76
- data/lib/punchblock/command/hangup.rb +3 -19
- data/lib/punchblock/command/join.rb +21 -70
- data/lib/punchblock/command/mute.rb +3 -3
- data/lib/punchblock/command/redirect.rb +6 -39
- data/lib/punchblock/command/reject.rb +14 -54
- data/lib/punchblock/command/unjoin.rb +8 -40
- data/lib/punchblock/command/unmute.rb +3 -3
- data/lib/punchblock/command_node.rb +0 -17
- data/lib/punchblock/component/asterisk/agi/command.rb +20 -127
- data/lib/punchblock/component/asterisk/ami/action.rb +30 -117
- data/lib/punchblock/component/component_node.rb +1 -1
- data/lib/punchblock/component/input.rb +89 -268
- data/lib/punchblock/component/output.rb +106 -154
- data/lib/punchblock/component/prompt.rb +51 -0
- data/lib/punchblock/component/record.rb +41 -130
- data/lib/punchblock/component.rb +1 -0
- data/lib/punchblock/connection/asterisk.rb +31 -4
- data/lib/punchblock/connection/xmpp.rb +6 -14
- data/lib/punchblock/core_ext/blather/stanza.rb +1 -1
- data/lib/punchblock/event/active_speaker.rb +2 -10
- data/lib/punchblock/event/answered.rb +3 -3
- data/lib/punchblock/event/asterisk/ami/event.rb +15 -47
- data/lib/punchblock/event/complete.rb +26 -48
- data/lib/punchblock/event/dtmf.rb +3 -13
- data/lib/punchblock/event/end.rb +10 -11
- data/lib/punchblock/event/joined.rb +5 -25
- data/lib/punchblock/event/offer.rb +4 -25
- data/lib/punchblock/event/ringing.rb +3 -3
- data/lib/punchblock/event/unjoined.rb +5 -25
- data/lib/punchblock/event.rb +0 -10
- data/lib/punchblock/has_headers.rb +20 -26
- data/lib/punchblock/rayo_node.rb +46 -23
- data/lib/punchblock/ref.rb +39 -18
- data/lib/punchblock/translator/asterisk/agi_app.rb +15 -0
- data/lib/punchblock/translator/asterisk/agi_command.rb +3 -1
- data/lib/punchblock/translator/asterisk/ami_error_converter.rb +20 -0
- data/lib/punchblock/translator/asterisk/call.rb +60 -39
- data/lib/punchblock/translator/asterisk/channel.rb +41 -0
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +4 -1
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +4 -4
- data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +62 -0
- data/lib/punchblock/translator/asterisk/component/input.rb +1 -0
- data/lib/punchblock/translator/asterisk/component/mrcp_native_prompt.rb +56 -0
- data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +53 -0
- data/lib/punchblock/translator/asterisk/component/mrcp_recog_prompt.rb +99 -0
- data/lib/punchblock/translator/asterisk/component/output.rb +30 -22
- data/lib/punchblock/translator/asterisk/component/record.rb +8 -6
- data/lib/punchblock/translator/asterisk/component.rb +6 -5
- data/lib/punchblock/translator/asterisk/unimrcp_app.rb +26 -0
- data/lib/punchblock/translator/asterisk.rb +24 -28
- data/lib/punchblock/translator/dtmf_recognizer.rb +39 -20
- data/lib/punchblock/translator/freeswitch/call.rb +15 -14
- data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +5 -4
- data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
- data/lib/punchblock/translator/freeswitch/component/input.rb +5 -0
- data/lib/punchblock/translator/freeswitch/component/output.rb +2 -2
- data/lib/punchblock/translator/freeswitch/component/record.rb +19 -13
- data/lib/punchblock/translator/freeswitch/component/tts_output.rb +2 -2
- data/lib/punchblock/translator/freeswitch/component.rb +2 -5
- data/lib/punchblock/translator/freeswitch.rb +2 -2
- data/lib/punchblock/translator/input_component.rb +33 -13
- data/lib/punchblock/uri_list.rb +21 -0
- data/lib/punchblock/version.rb +1 -1
- data/lib/punchblock.rb +4 -3
- data/punchblock.gemspec +7 -3
- data/spec/punchblock/client/component_registry_spec.rb +1 -1
- data/spec/punchblock/client_spec.rb +10 -26
- data/spec/punchblock/command/accept_spec.rb +41 -7
- data/spec/punchblock/command/answer_spec.rb +51 -7
- data/spec/punchblock/command/dial_spec.rb +56 -14
- data/spec/punchblock/command/hangup_spec.rb +41 -7
- data/spec/punchblock/command/join_spec.rb +53 -11
- data/spec/punchblock/command/mute_spec.rb +19 -4
- data/spec/punchblock/command/redirect_spec.rb +40 -10
- data/spec/punchblock/command/reject_spec.rb +43 -11
- data/spec/punchblock/command/unjoin_spec.rb +40 -9
- data/spec/punchblock/command/unmute_spec.rb +19 -4
- data/spec/punchblock/command_node_spec.rb +0 -4
- data/spec/punchblock/component/asterisk/agi/command_spec.rb +16 -39
- data/spec/punchblock/component/asterisk/ami/action_spec.rb +50 -53
- data/spec/punchblock/component/component_node_spec.rb +3 -5
- data/spec/punchblock/component/input_spec.rb +194 -61
- data/spec/punchblock/component/output_spec.rb +194 -62
- data/spec/punchblock/component/prompt_spec.rb +132 -0
- data/spec/punchblock/component/record_spec.rb +70 -32
- data/spec/punchblock/connection/asterisk_spec.rb +17 -3
- data/spec/punchblock/connection/freeswitch_spec.rb +4 -4
- data/spec/punchblock/connection/xmpp_spec.rb +20 -38
- data/spec/punchblock/event/answered_spec.rb +12 -10
- data/spec/punchblock/event/asterisk/ami/event_spec.rb +27 -22
- data/spec/punchblock/event/complete_spec.rb +15 -19
- data/spec/punchblock/event/dtmf_spec.rb +5 -6
- data/spec/punchblock/event/end_spec.rb +20 -10
- data/spec/punchblock/event/joined_spec.rb +8 -7
- data/spec/punchblock/event/offer_spec.rb +41 -12
- data/spec/punchblock/event/ringing_spec.rb +12 -10
- data/spec/punchblock/event/started_speaking_spec.rb +5 -6
- data/spec/punchblock/event/stopped_speaking_spec.rb +5 -6
- data/spec/punchblock/event/unjoined_spec.rb +7 -7
- data/spec/punchblock/ref_spec.rb +86 -9
- data/spec/punchblock/translator/asterisk/call_spec.rb +317 -154
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +28 -5
- data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +15 -13
- data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +237 -0
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +171 -14
- data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +652 -0
- data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +646 -0
- data/spec/punchblock/translator/asterisk/component/output_spec.rb +127 -77
- data/spec/punchblock/translator/asterisk/component/record_spec.rb +17 -8
- data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +2 -2
- data/spec/punchblock/translator/asterisk/component_spec.rb +3 -7
- data/spec/punchblock/translator/asterisk_spec.rb +20 -24
- data/spec/punchblock/translator/freeswitch/call_spec.rb +103 -99
- data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +17 -8
- data/spec/punchblock/translator/freeswitch/component/input_spec.rb +26 -14
- data/spec/punchblock/translator/freeswitch/component/output_spec.rb +30 -52
- data/spec/punchblock/translator/freeswitch/component/record_spec.rb +23 -19
- data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +18 -8
- data/spec/punchblock/translator/freeswitch/component_spec.rb +4 -8
- data/spec/punchblock/translator/freeswitch_spec.rb +11 -14
- data/spec/punchblock/uri_list_spec.rb +49 -0
- data/spec/punchblock_spec.rb +11 -1
- data/spec/spec_helper.rb +7 -11
- data/spec/support/mock_connection_with_event_handler.rb +1 -1
- metadata +104 -24
- data/lib/punchblock/header.rb +0 -9
- data/lib/punchblock/key_value_pair_node.rb +0 -51
- data/spec/punchblock/header_spec.rb +0 -11
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'punchblock/translator/asterisk/unimrcp_app'
|
4
|
+
|
5
|
+
module Punchblock
|
6
|
+
module Translator
|
7
|
+
class Asterisk
|
8
|
+
module Component
|
9
|
+
module MRCPRecogPrompt
|
10
|
+
UniMRCPError = Class.new Punchblock::Error
|
11
|
+
|
12
|
+
def execute
|
13
|
+
setup_defaults
|
14
|
+
validate
|
15
|
+
send_ref
|
16
|
+
execute_unimrcp_app
|
17
|
+
complete
|
18
|
+
rescue ChannelGoneError
|
19
|
+
call_ended
|
20
|
+
rescue UniMRCPError
|
21
|
+
complete_with_error 'Terminated due to UniMRCP error'
|
22
|
+
rescue RubyAMI::Error => e
|
23
|
+
complete_with_error "Terminated due to AMI error '#{e.message}'"
|
24
|
+
rescue OptionError => e
|
25
|
+
with_error 'option error', e.message
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate
|
31
|
+
[:interrupt_on, :start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
|
32
|
+
raise OptionError, "A #{opt} value is unsupported on Asterisk." if output_node.send opt
|
33
|
+
end
|
34
|
+
|
35
|
+
raise OptionError, "An initial-timeout value must be -1 or a positive integer." if @initial_timeout < -1
|
36
|
+
raise OptionError, "An inter-digit-timeout value must be -1 or a positive integer." if @inter_digit_timeout < -1
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute_app(app, *args)
|
40
|
+
UniMRCPApp.new(app, *args, unimrcp_app_options).execute @call
|
41
|
+
raise UniMRCPError if @call.channel_var('RECOG_STATUS') == 'ERROR'
|
42
|
+
end
|
43
|
+
|
44
|
+
def unimrcp_app_options
|
45
|
+
{uer: 1, b: (@component_node.barge_in == false ? 0 : 1)}.tap do |opts|
|
46
|
+
opts[:nit] = @initial_timeout if @initial_timeout > -1
|
47
|
+
opts[:dit] = @inter_digit_timeout if @inter_digit_timeout > -1
|
48
|
+
opts[:dttc] = input_node.terminator if input_node.terminator
|
49
|
+
yield opts
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup_defaults
|
54
|
+
@initial_timeout = input_node.initial_timeout || -1
|
55
|
+
@inter_digit_timeout = input_node.inter_digit_timeout || -1
|
56
|
+
end
|
57
|
+
|
58
|
+
def grammars
|
59
|
+
input_node.grammars.map do |d|
|
60
|
+
if d.content_type
|
61
|
+
d.value.to_doc.to_s
|
62
|
+
else
|
63
|
+
d.url
|
64
|
+
end
|
65
|
+
end.join ','
|
66
|
+
end
|
67
|
+
|
68
|
+
def first_doc
|
69
|
+
output_node.render_documents.first
|
70
|
+
end
|
71
|
+
|
72
|
+
def audio_filename
|
73
|
+
first_doc.value.first
|
74
|
+
end
|
75
|
+
|
76
|
+
def output_node
|
77
|
+
@component_node.output
|
78
|
+
end
|
79
|
+
|
80
|
+
def input_node
|
81
|
+
@component_node.input
|
82
|
+
end
|
83
|
+
|
84
|
+
def complete
|
85
|
+
send_complete_event case @call.channel_var('RECOG_COMPLETION_CAUSE')
|
86
|
+
when '000'
|
87
|
+
nlsml = RubySpeech.parse URI.decode(@call.channel_var('RECOG_RESULT'))
|
88
|
+
Punchblock::Component::Input::Complete::Match.new nlsml: nlsml
|
89
|
+
when '001'
|
90
|
+
Punchblock::Component::Input::Complete::NoMatch.new
|
91
|
+
when '002'
|
92
|
+
Punchblock::Component::Input::Complete::NoInput.new
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
require 'active_support/core_ext/string/filters'
|
4
|
+
require 'punchblock/translator/asterisk/unimrcp_app'
|
4
5
|
|
5
6
|
module Punchblock
|
6
7
|
module Translator
|
@@ -10,14 +11,17 @@ module Punchblock
|
|
10
11
|
include StopByRedirect
|
11
12
|
|
12
13
|
UnrenderableDocError = Class.new OptionError
|
14
|
+
UniMRCPError = Class.new Punchblock::Error
|
15
|
+
PlaybackError = Class.new Punchblock::Error
|
13
16
|
|
14
17
|
def setup
|
15
18
|
@media_engine = @call.translator.media_engine
|
16
19
|
end
|
17
20
|
|
18
21
|
def execute
|
19
|
-
raise OptionError, 'An SSML document is required.' unless @component_node.
|
20
|
-
raise OptionError, '
|
22
|
+
raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value
|
23
|
+
raise OptionError, 'Only a single document is supported.' unless @component_node.render_documents.size == 1
|
24
|
+
raise OptionError, 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :voice
|
21
25
|
|
22
26
|
[:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
|
23
27
|
raise OptionError, "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
|
@@ -44,7 +48,7 @@ module Punchblock
|
|
44
48
|
if interrupt
|
45
49
|
output_component = current_actor
|
46
50
|
call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
|
47
|
-
output_component.stop_by_redirect
|
51
|
+
output_component.stop_by_redirect finish_reason
|
48
52
|
end
|
49
53
|
end
|
50
54
|
|
@@ -52,10 +56,12 @@ module Punchblock
|
|
52
56
|
|
53
57
|
opts = early ? "#{path},noanswer" : path
|
54
58
|
@call.execute_agi_command 'EXEC Playback', opts
|
59
|
+
raise PlaybackError if @call.channel_var('PLAYBACKSTATUS') == 'FAILED'
|
55
60
|
when :unimrcp
|
56
61
|
@call.send_progress if early
|
57
62
|
send_ref
|
58
|
-
|
63
|
+
UniMRCPApp.new('MRCPSynth', render_doc, mrcpsynth_options).execute @call
|
64
|
+
raise UniMRCPError if @call.channel_var('SYNTHSTATUS') == 'ERROR'
|
59
65
|
when :swift
|
60
66
|
@call.send_progress if early
|
61
67
|
send_ref
|
@@ -63,7 +69,13 @@ module Punchblock
|
|
63
69
|
else
|
64
70
|
raise OptionError, "The renderer #{rendering_engine} is unsupported."
|
65
71
|
end
|
66
|
-
|
72
|
+
send_finish
|
73
|
+
rescue ChannelGoneError
|
74
|
+
call_ended
|
75
|
+
rescue PlaybackError
|
76
|
+
complete_with_error 'Terminated due to playback error'
|
77
|
+
rescue UniMRCPError
|
78
|
+
complete_with_error 'Terminated due to UniMRCP error'
|
67
79
|
rescue RubyAMI::Error => e
|
68
80
|
complete_with_error "Terminated due to AMI error '#{e.message}'"
|
69
81
|
rescue UnrenderableDocError => e
|
@@ -75,10 +87,10 @@ module Punchblock
|
|
75
87
|
private
|
76
88
|
|
77
89
|
def filenames
|
78
|
-
@filenames ||=
|
90
|
+
@filenames ||= render_doc.children.map do |node|
|
79
91
|
case node
|
80
92
|
when RubySpeech::SSML::Audio
|
81
|
-
node.src
|
93
|
+
node.src.sub('file://', '')
|
82
94
|
when String
|
83
95
|
raise if node.include?(' ')
|
84
96
|
node
|
@@ -90,34 +102,30 @@ module Punchblock
|
|
90
102
|
raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.'
|
91
103
|
end
|
92
104
|
|
93
|
-
def
|
94
|
-
@component_node.
|
95
|
-
end
|
96
|
-
|
97
|
-
def escape_commas(text)
|
98
|
-
text.gsub(',', '\\,')
|
105
|
+
def render_doc
|
106
|
+
@component_node.render_documents.first.value
|
99
107
|
end
|
100
108
|
|
101
109
|
def mrcpsynth_options
|
102
|
-
|
103
|
-
opts
|
104
|
-
opts
|
105
|
-
end
|
110
|
+
{}.tap do |opts|
|
111
|
+
opts[:i] = 'any' if [:any, :dtmf].include? @component_node.interrupt_on
|
112
|
+
opts[:v] = @component_node.voice if @component_node.voice
|
113
|
+
end
|
106
114
|
end
|
107
115
|
|
108
116
|
def swift_doc
|
109
|
-
doc =
|
117
|
+
doc = render_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
|
110
118
|
doc << "|1|1" if [:any, :dtmf].include? @component_node.interrupt_on
|
111
119
|
doc.insert 0, "#{@component_node.voice}^" if @component_node.voice
|
112
120
|
doc
|
113
121
|
end
|
114
122
|
|
115
|
-
def
|
116
|
-
send_complete_event
|
123
|
+
def send_finish
|
124
|
+
send_complete_event finish_reason
|
117
125
|
end
|
118
126
|
|
119
|
-
def
|
120
|
-
Punchblock::Component::Output::Complete::
|
127
|
+
def finish_reason
|
128
|
+
Punchblock::Component::Output::Complete::Finish.new
|
121
129
|
end
|
122
130
|
end
|
123
131
|
end
|
@@ -27,8 +27,6 @@ module Punchblock
|
|
27
27
|
component.finished
|
28
28
|
end
|
29
29
|
|
30
|
-
send_ref
|
31
|
-
|
32
30
|
if @component_node.start_beep
|
33
31
|
@call.execute_agi_command 'STREAM FILE', 'beep', '""'
|
34
32
|
end
|
@@ -39,8 +37,12 @@ module Punchblock
|
|
39
37
|
ami_client.send_action 'StopMonitor', 'Channel' => call.channel
|
40
38
|
end
|
41
39
|
end
|
40
|
+
|
41
|
+
send_ref
|
42
|
+
rescue ChannelGoneError
|
43
|
+
set_node_response ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{call_id}", call_id)
|
42
44
|
rescue RubyAMI::Error => e
|
43
|
-
|
45
|
+
with_error :platform_error, "Terminated due to AMI error '#{e.message}'"
|
44
46
|
rescue OptionError => e
|
45
47
|
with_error 'option error', e.message
|
46
48
|
end
|
@@ -63,7 +65,7 @@ module Punchblock
|
|
63
65
|
end
|
64
66
|
|
65
67
|
def finished
|
66
|
-
send_complete_event(@complete_reason ||
|
68
|
+
send_complete_event(@complete_reason || max_duration_reason)
|
67
69
|
end
|
68
70
|
|
69
71
|
private
|
@@ -80,8 +82,8 @@ module Punchblock
|
|
80
82
|
Punchblock::Event::Complete::Stop.new
|
81
83
|
end
|
82
84
|
|
83
|
-
def
|
84
|
-
Punchblock::Component::Record::Complete::
|
85
|
+
def max_duration_reason
|
86
|
+
Punchblock::Component::Record::Complete::MaxDuration.new
|
85
87
|
end
|
86
88
|
|
87
89
|
def send_complete_event(reason)
|
@@ -7,8 +7,12 @@ module Punchblock
|
|
7
7
|
extend ActiveSupport::Autoload
|
8
8
|
|
9
9
|
autoload :Asterisk
|
10
|
+
autoload :ComposedPrompt
|
10
11
|
autoload :Input
|
11
12
|
autoload :Output
|
13
|
+
autoload :MRCPPrompt
|
14
|
+
autoload :MRCPNativePrompt
|
15
|
+
autoload :MRCPRecogPrompt
|
12
16
|
autoload :Record
|
13
17
|
autoload :StopByRedirect
|
14
18
|
|
@@ -36,10 +40,7 @@ module Punchblock
|
|
36
40
|
def send_complete_event(reason, recording = nil, should_terminate = true)
|
37
41
|
return if @complete
|
38
42
|
@complete = true
|
39
|
-
event = Punchblock::Event::Complete.new
|
40
|
-
c.reason = reason
|
41
|
-
c << recording if recording
|
42
|
-
end
|
43
|
+
event = Punchblock::Event::Complete.new reason: reason, recording: recording
|
43
44
|
send_event event
|
44
45
|
terminate if should_terminate
|
45
46
|
end
|
@@ -73,7 +74,7 @@ module Punchblock
|
|
73
74
|
end
|
74
75
|
|
75
76
|
def send_ref
|
76
|
-
set_node_response Ref.new :
|
77
|
+
set_node_response Ref.new uri: id
|
77
78
|
end
|
78
79
|
|
79
80
|
def with_error(name, text)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'punchblock/translator/asterisk/agi_app'
|
2
|
+
require 'active_support/core_ext/string/filters'
|
3
|
+
|
4
|
+
module Punchblock
|
5
|
+
module Translator
|
6
|
+
class Asterisk
|
7
|
+
class UniMRCPApp
|
8
|
+
def initialize(app, *args, options)
|
9
|
+
args.map! { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }
|
10
|
+
args << prepare_options(options)
|
11
|
+
@agi_app = AGIApp.new(app, *args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(call)
|
15
|
+
@agi_app.execute call
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def prepare_options(options)
|
21
|
+
options.map { |o| o.join '=' }.join '&'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -8,10 +8,14 @@ module Punchblock
|
|
8
8
|
class Asterisk
|
9
9
|
include Celluloid
|
10
10
|
|
11
|
+
# Indicates that a command was executed against a channel which no longer exists
|
12
|
+
ChannelGoneError = Class.new Punchblock::Error
|
13
|
+
|
11
14
|
extend ActiveSupport::Autoload
|
12
15
|
|
13
16
|
autoload :AGICommand
|
14
17
|
autoload :Call
|
18
|
+
autoload :Channel
|
15
19
|
autoload :Component
|
16
20
|
|
17
21
|
attr_reader :ami_client, :connection, :media_engine, :calls
|
@@ -20,15 +24,13 @@ module Punchblock
|
|
20
24
|
REDIRECT_EXTENSION = '1'
|
21
25
|
REDIRECT_PRIORITY = '1'
|
22
26
|
|
23
|
-
|
24
|
-
EVENTS_ALLOWED_BRIDGED = %w{agiexec asyncagi}
|
27
|
+
EVENTS_ALLOWED_BRIDGED = %w{AGIExec AsyncAGI}
|
25
28
|
|
26
29
|
trap_exit :actor_died
|
27
30
|
|
28
31
|
def initialize(ami_client, connection, media_engine = nil)
|
29
32
|
@ami_client, @connection, @media_engine = ami_client, connection, media_engine
|
30
33
|
@calls, @components, @channel_to_call_id = {}, {}, {}
|
31
|
-
@fully_booted_count = 0
|
32
34
|
end
|
33
35
|
|
34
36
|
def register_call(call)
|
@@ -36,9 +38,9 @@ module Punchblock
|
|
36
38
|
@calls[call.id] ||= call
|
37
39
|
end
|
38
40
|
|
39
|
-
def deregister_call(
|
40
|
-
@channel_to_call_id.delete
|
41
|
-
@calls.delete
|
41
|
+
def deregister_call(id, channel)
|
42
|
+
@channel_to_call_id.delete channel
|
43
|
+
@calls.delete id
|
42
44
|
end
|
43
45
|
|
44
46
|
def call_with_id(call_id)
|
@@ -46,7 +48,7 @@ module Punchblock
|
|
46
48
|
end
|
47
49
|
|
48
50
|
def call_for_channel(channel)
|
49
|
-
call_with_id @channel_to_call_id[
|
51
|
+
call_with_id @channel_to_call_id[Channel.new(channel).name]
|
50
52
|
end
|
51
53
|
|
52
54
|
def register_component(component)
|
@@ -65,7 +67,7 @@ module Punchblock
|
|
65
67
|
def handle_ami_event(event)
|
66
68
|
return unless event.is_a? RubyAMI::Event
|
67
69
|
|
68
|
-
if event.name
|
70
|
+
if event.name == 'FullyBooted'
|
69
71
|
handle_pb_event Connection::Connected.new
|
70
72
|
run_at_fully_booted
|
71
73
|
return
|
@@ -76,7 +78,7 @@ module Punchblock
|
|
76
78
|
ami_dispatch_to_or_create_call event
|
77
79
|
|
78
80
|
unless ami_event_known_call?(event)
|
79
|
-
handle_pb_event Event::Asterisk::AMI::Event.new(:
|
81
|
+
handle_pb_event Event::Asterisk::AMI::Event.new(name: event.name, headers: event.headers)
|
80
82
|
end
|
81
83
|
end
|
82
84
|
exclusive :handle_ami_event
|
@@ -171,24 +173,24 @@ module Punchblock
|
|
171
173
|
end
|
172
174
|
|
173
175
|
def ami_dispatch_to_or_create_call(event)
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
176
|
+
calls_for_event = channels_for_ami_event(event).inject({}) do |h, channel|
|
177
|
+
call = call_for_channel channel
|
178
|
+
h[channel] = call if call
|
179
|
+
h
|
180
|
+
end
|
181
|
+
|
182
|
+
if !calls_for_event.empty?
|
183
|
+
calls_for_event.each_pair do |channel, call|
|
184
|
+
next if channel.bridged? && !EVENTS_ALLOWED_BRIDGED.include?(event.name)
|
185
|
+
call.async.process_ami_event event
|
184
186
|
end
|
185
|
-
elsif event.name
|
187
|
+
elsif event.name == "AsyncAGI" && event['SubEvent'] == "Start"
|
186
188
|
handle_async_agi_start_event event
|
187
189
|
end
|
188
190
|
end
|
189
191
|
|
190
192
|
def channels_for_ami_event(event)
|
191
|
-
[event['Channel'], event['Channel1'], event['Channel2']].compact
|
193
|
+
[event['Channel'], event['Channel1'], event['Channel2']].compact.map { |channel| Channel.new(channel) }
|
192
194
|
end
|
193
195
|
|
194
196
|
def ami_event_known_call?(event)
|
@@ -197,18 +199,12 @@ module Punchblock
|
|
197
199
|
(event['Channel2'] && call_for_channel(event['Channel2']))
|
198
200
|
end
|
199
201
|
|
200
|
-
def channel_is_bridged?(channel)
|
201
|
-
matches = channel.match CHANNEL_NORMALIZATION_REGEXP
|
202
|
-
matches[:prefix] || matches[:suffix]
|
203
|
-
end
|
204
|
-
|
205
202
|
def handle_async_agi_start_event(event)
|
206
203
|
env = RubyAMI::AsyncAGIEnvironmentParser.new(event['Env']).to_hash
|
207
204
|
|
208
205
|
return if env[:agi_extension] == 'h' || env[:agi_type] == 'Kill'
|
209
206
|
|
210
|
-
call = Call.
|
211
|
-
link call
|
207
|
+
call = Call.new_link event['Channel'], current_actor, ami_client, connection, env
|
212
208
|
register_call call
|
213
209
|
call.async.send_offer
|
214
210
|
end
|
@@ -3,42 +3,45 @@
|
|
3
3
|
module Punchblock
|
4
4
|
module Translator
|
5
5
|
class DTMFRecognizer
|
6
|
-
def initialize(responder, grammar, initial_timeout = nil, inter_digit_timeout = nil)
|
6
|
+
def initialize(responder, grammar, initial_timeout = nil, inter_digit_timeout = nil, terminator = nil)
|
7
7
|
@responder = responder
|
8
|
-
self.grammar = grammar
|
9
8
|
self.initial_timeout = initial_timeout || -1
|
10
9
|
self.inter_digit_timeout = inter_digit_timeout || -1
|
10
|
+
@terminator = terminator
|
11
|
+
@finished = false
|
11
12
|
|
13
|
+
@matcher = RubySpeech::GRXML::Matcher.new RubySpeech::GRXML.import(grammar.to_s)
|
12
14
|
@buffer = ""
|
13
|
-
|
14
|
-
begin_initial_timer @initial_timeout/1000 unless @initial_timeout == -1
|
15
15
|
end
|
16
16
|
|
17
17
|
def <<(digit)
|
18
|
-
@buffer << digit
|
19
18
|
cancel_initial_timer
|
20
|
-
|
21
|
-
|
22
|
-
@responder.match match.mode, match.confidence, match.utterance, match.interpretation
|
19
|
+
@buffer << digit unless terminating?(digit)
|
20
|
+
case (match = get_match)
|
23
21
|
when RubySpeech::GRXML::NoMatch
|
24
|
-
|
22
|
+
finalize :nomatch
|
23
|
+
when RubySpeech::GRXML::MaxMatch
|
24
|
+
finalize :match, match
|
25
|
+
when RubySpeech::GRXML::Match
|
26
|
+
finalize :match, match if terminating?(digit)
|
25
27
|
when RubySpeech::GRXML::PotentialMatch
|
26
|
-
|
28
|
+
finalize :nomatch if terminating?(digit)
|
27
29
|
end
|
30
|
+
reset_inter_digit_timer unless @finished
|
28
31
|
end
|
29
32
|
|
30
|
-
def
|
31
|
-
|
32
|
-
cancel_inter_digit_timer
|
33
|
+
def start_timers
|
34
|
+
begin_initial_timer @initial_timeout/1000 unless @initial_timeout == -1
|
33
35
|
end
|
34
36
|
|
35
37
|
private
|
36
38
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
def terminating?(digit)
|
40
|
+
digit == @terminator
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_match
|
44
|
+
@matcher.match @buffer.dup
|
42
45
|
end
|
43
46
|
|
44
47
|
def after(*args, &block)
|
@@ -57,7 +60,7 @@ module Punchblock
|
|
57
60
|
|
58
61
|
def begin_initial_timer(timeout)
|
59
62
|
@initial_timer = after timeout do
|
60
|
-
|
63
|
+
finalize :noinput
|
61
64
|
end
|
62
65
|
end
|
63
66
|
|
@@ -71,7 +74,12 @@ module Punchblock
|
|
71
74
|
return if @inter_digit_timeout == -1
|
72
75
|
@inter_digit_timer ||= begin
|
73
76
|
after @inter_digit_timeout/1000 do
|
74
|
-
|
77
|
+
case (match = get_match)
|
78
|
+
when RubySpeech::GRXML::Match
|
79
|
+
finalize :match, match
|
80
|
+
else
|
81
|
+
finalize :nomatch
|
82
|
+
end
|
75
83
|
end
|
76
84
|
end
|
77
85
|
@inter_digit_timer.reset
|
@@ -82,6 +90,17 @@ module Punchblock
|
|
82
90
|
@inter_digit_timer.cancel
|
83
91
|
@inter_digit_timer = nil
|
84
92
|
end
|
93
|
+
|
94
|
+
def finalize(match_type, match = nil)
|
95
|
+
cancel_initial_timer
|
96
|
+
cancel_inter_digit_timer
|
97
|
+
if match
|
98
|
+
@responder.send match_type, match
|
99
|
+
else
|
100
|
+
@responder.send match_type
|
101
|
+
end
|
102
|
+
@finished = true
|
103
|
+
end
|
85
104
|
end
|
86
105
|
end
|
87
106
|
end
|
@@ -14,9 +14,10 @@ module Punchblock
|
|
14
14
|
HANGUP_CAUSE_TO_END_REASON = Hash.new :error
|
15
15
|
|
16
16
|
HANGUP_CAUSE_TO_END_REASON['USER_BUSY'] = :busy
|
17
|
+
HANGUP_CAUSE_TO_END_REASON['MANAGER_REQUEST'] = :hangup_command
|
17
18
|
|
18
19
|
%w{
|
19
|
-
NORMAL_CLEARING ORIGINATOR_CANCEL SYSTEM_SHUTDOWN
|
20
|
+
NORMAL_CLEARING ORIGINATOR_CANCEL SYSTEM_SHUTDOWN
|
20
21
|
BLIND_TRANSFER ATTENDED_TRANSFER PICKED_OFF NORMAL_UNSPECIFIED
|
21
22
|
}.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :hangup }
|
22
23
|
|
@@ -91,16 +92,16 @@ module Punchblock
|
|
91
92
|
command = @pending_joins[event[:other_leg_unique_id]]
|
92
93
|
command.response = true if command
|
93
94
|
|
94
|
-
|
95
|
-
send_pb_event Event::Joined.new(:
|
95
|
+
other_call_uri = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
|
96
|
+
send_pb_event Event::Joined.new(:call_uri => other_call_uri)
|
96
97
|
end
|
97
98
|
|
98
99
|
register_handler :es, :event_name => 'CHANNEL_UNBRIDGE' do |event|
|
99
100
|
command = @pending_unjoins[event[:other_leg_unique_id]]
|
100
101
|
command.response = true if command
|
101
102
|
|
102
|
-
|
103
|
-
send_pb_event Event::Unjoined.new(:
|
103
|
+
other_call_uri = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
|
104
|
+
send_pb_event Event::Unjoined.new(:call_uri => other_call_uri)
|
104
105
|
end
|
105
106
|
|
106
107
|
register_handler :es, [:has_key?, :scope_variable_punchblock_component_id] => true do |event|
|
@@ -145,8 +146,8 @@ module Punchblock
|
|
145
146
|
options[:origination_caller_id_number] = "'#{cid_number}'" if cid_number.present?
|
146
147
|
options[:origination_caller_id_name] = "'#{cid_name}'" if cid_name.present?
|
147
148
|
options[:originate_timeout] = dial_command.timeout/1000 if dial_command.timeout
|
148
|
-
dial_command.headers.each do |
|
149
|
-
options["sip_h_#{
|
149
|
+
dial_command.headers.each do |name, value|
|
150
|
+
options["sip_h_#{name}"] = "'#{value}'"
|
150
151
|
end
|
151
152
|
opts = options.inject([]) do |a, (k, v)|
|
152
153
|
a << "#{k}=#{v}"
|
@@ -154,7 +155,7 @@ module Punchblock
|
|
154
155
|
|
155
156
|
stream.bgapi "originate {#{opts}}#{dial_command.to} &park()"
|
156
157
|
|
157
|
-
dial_command.response = Ref.new :
|
158
|
+
dial_command.response = Ref.new uri: id
|
158
159
|
end
|
159
160
|
|
160
161
|
def outbound?
|
@@ -195,10 +196,10 @@ module Punchblock
|
|
195
196
|
hangup
|
196
197
|
command.response = true
|
197
198
|
when Command::Join
|
198
|
-
@pending_joins[command.
|
199
|
-
uuid_foo :bridge, command.
|
199
|
+
@pending_joins[command.call_uri] = command
|
200
|
+
uuid_foo :bridge, command.call_uri
|
200
201
|
when Command::Unjoin
|
201
|
-
@pending_unjoins[command.
|
202
|
+
@pending_unjoins[command.call_uri] = command
|
202
203
|
uuid_foo :transfer, '-both park inline'
|
203
204
|
when Command::Reject
|
204
205
|
hangup REJECT_TO_HANGUP_REASON[command.reason]
|
@@ -222,7 +223,7 @@ module Punchblock
|
|
222
223
|
end
|
223
224
|
end
|
224
225
|
|
225
|
-
def hangup(reason = '
|
226
|
+
def hangup(reason = 'MANAGER_REQUEST')
|
226
227
|
sendmsg :call_command => 'hangup', :hangup_cause => reason
|
227
228
|
end
|
228
229
|
|
@@ -244,7 +245,7 @@ module Punchblock
|
|
244
245
|
|
245
246
|
def send_end_event(reason)
|
246
247
|
send_pb_event Event::End.new(:reason => reason)
|
247
|
-
translator.deregister_call
|
248
|
+
translator.deregister_call id
|
248
249
|
terminate
|
249
250
|
end
|
250
251
|
|
@@ -268,7 +269,7 @@ module Punchblock
|
|
268
269
|
|
269
270
|
def headers
|
270
271
|
es_env.to_a.inject({}) do |accumulator, element|
|
271
|
-
accumulator[
|
272
|
+
accumulator['X-' + element[0].to_s] = element[1] || ''
|
272
273
|
accumulator
|
273
274
|
end
|
274
275
|
end
|
@@ -35,20 +35,21 @@ module Punchblock
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def validate
|
38
|
-
raise OptionError, 'An SSML document is required.' unless @component_node.
|
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
|
39
40
|
|
40
41
|
[:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
|
41
42
|
raise OptionError, "A #{opt} value is unsupported." if @component_node.send opt
|
42
43
|
end
|
43
44
|
|
44
45
|
case @component_node.interrupt_on
|
45
|
-
when :
|
46
|
+
when :voice, :dtmf, :any
|
46
47
|
raise OptionError, "An interrupt-on value of #{@component_node.interrupt_on} is unsupported."
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
|
-
def
|
51
|
-
Punchblock::Component::Output::Complete::
|
51
|
+
def finish_reason
|
52
|
+
Punchblock::Component::Output::Complete::Finish.new
|
52
53
|
end
|
53
54
|
end
|
54
55
|
end
|